Python Forum
[WxPython] Search window position in Trelby
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[WxPython] Search window position in Trelby
#1
Hi,
First off, I'm a writer and know nothing about coding and Python.
I use a writing program named Trelby, which is written in Python.
I want to modify the default position of the "find & replace" window, which now pops up smack in the middle of the page. I assume I can modify the setting in the finddlg.py file, whose first part looks as follows:

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
Can anyone tell me how to modify the code so that the default position of the window would be on the side of the page?

Thanks.
Reply
#2
need to change:
gsizer.Add(wx.StaticText(self, -1, "Find what:"), 0,
               wx.ALIGN_CENTER_VERTICAL)
to:
gsizer.Add(wx.StaticText(self, id=wx.ID_ANY, label="Find what:", POS=(10,10)), 0,
               wx.ALIGN_CENTER_VERTICAL)
WHERE pos=(10,10) are x and y position

make similar change to other widgets
Reply
#3
Larz60+
Thanks for your answer.
I made the change as indicated (just those 2 lines) on a test partition (better safe than sorry).
The Find windows now does not function and, running Trelby in the terminal, I get the following error:

Traceback (most recent call last):
File "/usr/share/trelby//src/trelby.py", line 2383, in OnFind
self.panel.ctrl.OnFind()
File "/usr/share/trelby//src/trelby.py", line 1007, in OnFind
dlg = finddlg.FindDlg(mainFrame, self)
File "/usr/share/trelby//src/finddlg.py", line 27, in __init__
gsizer.Add(wx.StaticText(self, id=wx.ID_ANY, label="Find what:", POS=(10,10)), 0,
File "/usr/lib/python2.7/dist-packages/wx-3.0-gtk3/wx/_controls.py", line 997, in __init__
_controls_.StaticText_swiginit(self,_controls_.new_StaticText(*args, **kwargs))
TypeError: 'POS' is an invalid keyword argument for this function

To make it easier, I here post the whole finddlg.py code:

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)

        wx.EVT_BUTTON(self, find.GetId(), self.OnFind)
        wx.EVT_BUTTON(self, replace.GetId(), self.OnReplace)
        wx.EVT_BUTTON(self, replaceAll.GetId(), self.OnReplaceAll)
        wx.EVT_BUTTON(self, self.moreButton.GetId(), self.OnMore)

        gutil.btnDblClick(find, self.OnFind)
        gutil.btnDblClick(replace, self.OnReplace)

        wx.EVT_TEXT(self, self.findEntry.GetId(), self.OnText)

        wx.EVT_TEXT_ENTER(self, self.findEntry.GetId(), self.OnFind)
        wx.EVT_TEXT_ENTER(self, self.replaceEntry.GetId(), self.OnFind)

        wx.EVT_CHAR(self, self.OnCharMisc)
        wx.EVT_CHAR(self.findEntry, self.OnCharEntry)
        wx.EVT_CHAR(self.replaceEntry, self.OnCharEntry)
        wx.EVT_CHAR(find, self.OnCharButton)
        wx.EVT_CHAR(replace, self.OnCharButton)
        wx.EVT_CHAR(replaceAll, self.OnCharButton)
        wx.EVT_CHAR(self.moreButton, self.OnCharButton)
        wx.EVT_CHAR(self.matchWholeCb, self.OnCharMisc)
        wx.EVT_CHAR(self.matchCaseCb, self.OnCharMisc)
        wx.EVT_CHAR(self.direction, self.OnCharMisc)
        wx.EVT_CHAR(self.elements, 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
                    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)
Reply
#4
I must have hit the caps lock, (new gaming keyboard) pos is lower case
Reply
#5
Changed POS to pos.
Now it runs fine, but window pops up in the center as before.
I tried to change the 10,10 values but nothing changed, like the default position is unaffected by the change in the line of code.
Reply
#6
could you give me a snippet of code that I can run, (with stubs for config, gutil, misc and undo)
Reply
#7
I hope I understood you alright and this is what you asked.
I took this code from files in the trelby/src directory

CONFIG.PY

# see fileformat.txt for more detailed information about the various
# defines found here.

from error import *
import misc
import mypickle
import pml
import screenplay
import util

import copy
import os

if "TRELBY_TESTING" in os.environ:
    import mock
    wx = mock.Mock()
else:
    import wx

# mapping from character to linebreak
_char2lb = {
    '>' : screenplay.LB_SPACE,
    '+' : screenplay.LB_SPACE2,
    '&' : screenplay.LB_NONE,
    '|' : screenplay.LB_FORCED,
    '.' : screenplay.LB_LAST
    }

# reverse to above
_lb2char = {}

# what string each linebreak type should be mapped to.
_lb2str = {
    screenplay.LB_SPACE  : " ",
    screenplay.LB_SPACE2 : "  ",
    screenplay.LB_NONE   : "",
    screenplay.LB_FORCED : "\n",
    screenplay.LB_LAST   : "\n"
    }

# contains a TypeInfo for each element type
_ti = []

# mapping from character to TypeInfo
_char2ti = {}

# mapping from line type to TypeInfo
_lt2ti = {}

# mapping from element name to TypeInfo
_name2ti = {}

# page break indicators. do not change these values as they're saved to
# the config file.
PBI_NONE = 0
PBI_REAL = 1
PBI_REAL_AND_UNADJ = 2

# for range checking above value
PBI_FIRST, PBI_LAST = PBI_NONE, PBI_REAL_AND_UNADJ

# constants for identifying PDFFontInfos
PDF_FONT_NORMAL = "Normal"
PDF_FONT_BOLD = "Bold"
PDF_FONT_ITALIC = "Italic"
PDF_FONT_BOLD_ITALIC = "Bold-Italic"

# scrolling  directions
SCROLL_UP = 0
SCROLL_DOWN = 1
SCROLL_CENTER = 2

# construct reverse lookup tables

for k, v in _char2lb.items():
    _lb2char[v] = k

del k, v

# non-changing information about an element type
class TypeInfo:
    def __init__(self, lt, char, name):

        # line type, e.g. screenplay.ACTION
        self.lt = lt

        # character used in saved scripts, e.g. "."
        self.char = char

        # textual name, e.g. "Action"
        self.name = name

# text type
class TextType:
    cvars = None

    def __init__(self):
        if not self.__class__.cvars:
            v = self.__class__.cvars = mypickle.Vars()

            v.addBool("isCaps", False, "AllCaps")
            v.addBool("isBold", False, "Bold")
            v.addBool("isItalic", False, "Italic")
            v.addBool("isUnderlined", False, "Underlined")

        self.__class__.cvars.setDefaults(self)

    def save(self, prefix):
        return self.cvars.save(prefix, self)

    def load(self, vals, prefix):
        self.cvars.load(vals, prefix, self)

# script-specific information about an element type
class Type:
    cvars = None

    def __init__(self, lt):

        # line type
        self.lt = lt

        # pointer to TypeInfo
        self.ti = lt2ti(lt)

        # text types, one for screen and one for export
        self.screen = TextType()
        self.export = TextType()

        if not self.__class__.cvars:
            v = self.__class__.cvars = mypickle.Vars()

            # these two are how much empty space to insert a) before the
            # element b) between the element's lines, in units of line /
            # 10.
            v.addInt("beforeSpacing", 0, "BeforeSpacing", 0, 50)
            v.addInt("intraSpacing", 0, "IntraSpacing", 0, 20)

            v.addInt("indent", 0, "Indent", 0, 80)
            v.addInt("width", 5, "Width", 5, 80)

            v.makeDicts()

        self.__class__.cvars.setDefaults(self)

    def save(self, prefix):
        prefix += "%s/" % self.ti.name

        s = self.cvars.save(prefix, self)
        s += self.screen.save(prefix + "Screen/")
        s += self.export.save(prefix + "Export/")

        return s

    def load(self, vals, prefix):
        prefix += "%s/" % self.ti.name

        self.cvars.load(vals, prefix, self)
        self.screen.load(vals, prefix + "Screen/")
        self.export.load(vals, prefix + "Export/")

# global information about an element type
class TypeGlobal:
    cvars = None

    def __init__(self, lt):

        # line type
        self.lt = lt

        # pointer to TypeInfo
        self.ti = lt2ti(lt)

        if not self.__class__.cvars:
            v = self.__class__.cvars = mypickle.Vars()

            # what type of element to insert when user presses enter or tab.
            v.addElemName("newTypeEnter", screenplay.ACTION, "NewTypeEnter")
            v.addElemName("newTypeTab", screenplay.ACTION, "NewTypeTab")

            # what element to switch to when user hits tab / shift-tab.
            v.addElemName("nextTypeTab", screenplay.ACTION, "NextTypeTab")
            v.addElemName("prevTypeTab", screenplay.ACTION, "PrevTypeTab")

            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)

# command (an action in the main program)
class Command:
    cvars = None

    def __init__(self, name, desc, defKeys = [], isMovement = False,
                 isFixed = False, isMenu = False,
                 scrollDirection = SCROLL_CENTER):

        # name, e.g. "MoveLeft"
        self.name = name

        # textual description
        self.desc = desc

        # default keys (list of serialized util.Key objects (ints))
        self.defKeys = defKeys

        # is this a movement command
        self.isMovement = isMovement

        # some commands & their keys (Tab, Enter, Quit, etc) are fixed and
        # can't be changed
        self.isFixed = isFixed

        # is this a menu item
        self.isMenu = isMenu

        # which way the command wants to scroll the page
        self.scrollDirection = scrollDirection

        if not self.__class__.cvars:
            v = self.__class__.cvars = mypickle.Vars()

            v.addList("keys", [], "Keys",
                      mypickle.IntVar("", 0, "", 0, 9223372036854775808L))

            v.makeDicts()

        # this is not actually needed but let's keep it for consistency
        self.__class__.cvars.setDefaults(self)

        self.keys = copy.deepcopy(self.defKeys)

    def save(self, prefix):
        if self.isFixed:
            return ""

        prefix += "%s/" % self.name

        if len(self.keys) > 0:
            return self.cvars.save(prefix, self)
        else:
            self.keys.append(0)
            s = self.cvars.save(prefix, self)
            self.keys = []

            return s

    def load(self, vals, prefix):
        if self.isFixed:
            return

        prefix += "%s/" % self.name

        tmp = copy.deepcopy(self.keys)
        self.cvars.load(vals, prefix, self)

        if len(self.keys) == 0:
            # we have a new command in the program not found in the old
            # config file
            self.keys = tmp
        elif self.keys[0] == 0:
            self.keys = []

        # weed out invalid bindings
        tmp2 = self.keys
        self.keys = []

        for k in tmp2:
            k2 = util.Key.fromInt(k)
            if not k2.isValidInputChar():
                self.keys.append(k)

# information about one screen font
class FontInfo:
    def __init__(self):
        self.font = None

        # font width and height
        self.fx = 1
        self.fy = 1

# information about one PDF font
class PDFFontInfo:
    cvars = None

    # list of characters not allowed in pdfNames
    invalidChars = None

    def __init__(self, name, style):
        # our name for the font (one of the PDF_FONT_* constants)
        self.name = name

        # 2 lowest bits of pml.TextOp.flags
        self.style = style

        if not self.__class__.cvars:
            v = self.__class__.cvars = mypickle.Vars()

            # name to use in generated PDF file (CourierNew, MyFontBold,
            # etc.). if empty, use the default PDF Courier font.
            v.addStrLatin1("pdfName", "", "Name")

            # filename for the font to embed, or empty meaning don't
            # embed.
            v.addStrUnicode("filename", u"", "Filename")

            v.makeDicts()

            tmp = ""

            for i in range(256):
                # the OpenType font specification 1.4, of all places,
                # contains the most detailed discussion of characters
                # allowed in Postscript font names, in the section on
                # 'name' tables, describing name ID 6 (=Postscript name).
                if (i <= 32) or (i >= 127) or chr(i) in (
                    "[", "]", "(", ")", "{", "}", "<", ">", "/", "%"):
                    tmp += chr(i)

            self.__class__.invalidChars = tmp

        self.__class__.cvars.setDefaults(self)

    def save(self, prefix):
        prefix += "%s/" % self.name

        return self.cvars.save(prefix, self)

    def load(self, vals, prefix):
        prefix += "%s/" % self.name

        self.cvars.load(vals, prefix, self)

    # fix up invalid values.
    def refresh(self):
        self.pdfName = util.deleteChars(self.pdfName, self.invalidChars)

        # to avoid confused users not understanding why their embedded
        # font isn't working, put in an arbitrary font name if needed
        if self.filename and not self.pdfName:
            self.pdfName = "SampleFontName"

# per-script config, each script has its own one of these.
class Config:
    cvars = None

    def __init__(self):

        if not self.__class__.cvars:
            self.setupVars()

        self.__class__.cvars.setDefaults(self)

        # type configs, key = line type, value = Type
        self.types = { }

        # element types
        t = Type(screenplay.SCENE)
        t.beforeSpacing = 10
        t.indent = 0
        t.width = 60
        t.screen.isCaps = True
        t.screen.isBold = True
        t.export.isCaps = True
        self.types[t.lt] = t

        t = Type(screenplay.ACTION)
        t.beforeSpacing = 10
        t.indent = 0
        t.width = 60
        self.types[t.lt] = t

        t = Type(screenplay.CHARACTER)
        t.beforeSpacing = 10
        t.indent = 22
        t.width = 38
        t.screen.isCaps = True
        t.export.isCaps = True
        self.types[t.lt] = t

        t = Type(screenplay.DIALOGUE)
        t.indent = 10
        t.width = 35
        self.types[t.lt] = t

        t = Type(screenplay.PAREN)
        t.indent = 16
        t.width = 25
        self.types[t.lt] = t

        t = Type(screenplay.TRANSITION)
        t.beforeSpacing = 10
        t.indent = 45
        t.width = 20
        t.screen.isCaps = True
        t.export.isCaps = True
        self.types[t.lt] = t

        t = Type(screenplay.SHOT)
        t.beforeSpacing = 10
        t.indent = 0
        t.width = 60
        t.screen.isCaps = True
        t.export.isCaps = True
        self.types[t.lt] = t

        t = Type(screenplay.ACTBREAK)
        t.beforeSpacing = 10
        t.indent = 25
        t.width = 10
        t.screen.isCaps = True
        t.screen.isBold = True
        t.screen.isUnderlined = True
        t.export.isCaps = True
        t.export.isUnderlined = True
        self.types[t.lt] = t

        t = Type(screenplay.NOTE)
        t.beforeSpacing = 10
        t.indent = 5
        t.width = 55
        t.screen.isItalic = True
        t.export.isItalic = True
        self.types[t.lt] = t

        # pdf font configs, key = PDF_FONT_*, value = PdfFontInfo
        self.pdfFonts = { }

        for name, style in (
            (PDF_FONT_NORMAL, pml.COURIER),
            (PDF_FONT_BOLD, pml.COURIER | pml.BOLD),
            (PDF_FONT_ITALIC, pml.COURIER | pml.ITALIC),
            (PDF_FONT_BOLD_ITALIC, pml.COURIER | pml.BOLD | pml.ITALIC)):
            self.pdfFonts[name] = PDFFontInfo(name, style)

        self.recalc()

    def setupVars(self):
        v = self.__class__.cvars = mypickle.Vars()

        # font size used for PDF generation, in points
        v.addInt("fontSize", 12, "FontSize", 4, 72)

        # margins
        v.addFloat("marginBottom", 25.4, "Margin/Bottom", 0.0, 900.0)
        v.addFloat("marginLeft", 38.1, "Margin/Left", 0.0, 900.0)
        v.addFloat("marginRight", 25.4, "Margin/Right", 0.0, 900.0)
        v.addFloat("marginTop", 12.7, "Margin/Top", 0.0, 900.0)

        # paper size
        v.addFloat("paperHeight", 297.0, "Paper/Height", 100.0, 1000.0)
        v.addFloat("paperWidth", 210.0, "Paper/Width", 50.0, 1000.0)

        # leave at least this many action lines on the end of a page
        v.addInt("pbActionLines", 2, "PageBreakActionLines", 1, 30)

        # leave at least this many dialogue lines on the end of a page
        v.addInt("pbDialogueLines", 2, "PageBreakDialogueLines", 1, 30)

        # whether scene continueds are enabled
        v.addBool("sceneContinueds", False, "SceneContinueds")

        # scene continued text indent width
        v.addInt("sceneContinuedIndent", 45, "SceneContinuedIndent", -20, 80)

        # whether to include scene numbers
        v.addBool("pdfShowSceneNumbers", False, "ShowSceneNumbers")

        # whether to include PDF TOC
        v.addBool("pdfIncludeTOC", True, "IncludeTOC")

        # whether to show PDF TOC by default
        v.addBool("pdfShowTOC", True, "ShowTOC")

        # whether to open PDF document on current page
        v.addBool("pdfOpenOnCurrentPage", True, "OpenOnCurrentPage")

        # whether to remove Note elements in PDF output
        v.addBool("pdfRemoveNotes", False, "RemoveNotes")

        # whether to draw rectangles around the outlines of Note elements
        v.addBool("pdfOutlineNotes", True, "OutlineNotes")

        # whether to draw rectangle showing margins
        v.addBool("pdfShowMargins", False, "ShowMargins")

        # whether to show line numbers next to each line
        v.addBool("pdfShowLineNumbers", False, "ShowLineNumbers")

        # cursor position, line
        v.addInt("cursorLine", 0, "Cursor/Line", 0, 1000000)

        # cursor position, column
        v.addInt("cursorColumn", 0, "Cursor/Column", 0, 1000000)

        # various strings we add to the script
        v.addStrLatin1("strMore", "(MORE)", "String/MoreDialogue")
        v.addStrLatin1("strContinuedPageEnd", "(CONTINUED)",
                       "String/ContinuedPageEnd")
        v.addStrLatin1("strContinuedPageStart", "CONTINUED:",
                       "String/ContinuedPageStart")
        v.addStrLatin1("strDialogueContinued", " (cont'd)",
                       "String/DialogueContinued")

        v.makeDicts()

    # 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 = self.cvars.makeVals(s)

        self.cvars.load(vals, "", self)

        for t in self.types.itervalues():
            t.load(vals, "Element/")

        for pf in self.pdfFonts.itervalues():
            pf.load(vals, "Font/")

        self.recalc()

    # save config into a string and return that.
    def save(self):
        s = self.cvars.save("", self)

        for t in self.types.itervalues():
            s += t.save("Element/")

        for pf in self.pdfFonts.itervalues():
            s += pf.save("Font/")

        return s

    # fix up all invalid config values and recalculate all variables
    # dependent on other variables.
    #
    # if doAll is False, enforces restrictions only on a per-variable
    # basis, e.g. doesn't modify variable v2 based on v1's value. this is
    # useful when user is interactively modifying v1, and it temporarily
    # strays out of bounds (e.g. when deleting the old text in an entry
    # box, thus getting the minimum value), which would then possibly
    # modify the value of other variables which is not what we want.
    def recalc(self, doAll = True):
        for it in self.cvars.numeric.itervalues():
            util.clampObj(self, it.name, it.minVal, it.maxVal)

        for el in self.types.itervalues():
            for it in el.cvars.numeric.itervalues():
                util.clampObj(el, it.name, it.minVal, it.maxVal)

        for it in self.cvars.stringLatin1.itervalues():
            setattr(self, it.name, util.toInputStr(getattr(self, it.name)))

        for pf in self.pdfFonts.itervalues():
            pf.refresh()

        # make sure usable space on the page isn't too small
        if doAll and (self.marginTop + self.marginBottom) >= \
               (self.paperHeight - 100.0):
            self.marginTop = 0.0
            self.marginBottom = 0.0

        h = self.paperHeight - self.marginTop - self.marginBottom

        # how many lines on a page
        self.linesOnPage = int(h / util.getTextHeight(self.fontSize))

    def getType(self, lt):
        return self.types[lt]

    # get a PDFFontInfo object for the given font type (PDF_FONT_*)
    def getPDFFont(self, fontType):
        return self.pdfFonts[fontType]

    # return a tuple of all the PDF font types
    def getPDFFontIds(self):
        return (PDF_FONT_NORMAL, PDF_FONT_BOLD, PDF_FONT_ITALIC,
                PDF_FONT_BOLD_ITALIC)

# global config. there is only ever one of these active.
class ConfigGlobal:
    cvars = None

    def __init__(self):

        if not self.__class__.cvars:
            self.setupVars()

        self.__class__.cvars.setDefaults(self)

        # type configs, key = line type, value = TypeGlobal
        self.types = { }

        # element types
        t = TypeGlobal(screenplay.SCENE)
        t.newTypeEnter = screenplay.ACTION
        t.newTypeTab = screenplay.CHARACTER
        t.nextTypeTab = screenplay.ACTION
        t.prevTypeTab = screenplay.TRANSITION
        self.types[t.lt] = t

        t = TypeGlobal(screenplay.ACTION)
        t.newTypeEnter = screenplay.ACTION
        t.newTypeTab = screenplay.CHARACTER
        t.nextTypeTab = screenplay.CHARACTER
        t.prevTypeTab = screenplay.CHARACTER
        self.types[t.lt] = t

        t = TypeGlobal(screenplay.CHARACTER)
        t.newTypeEnter = screenplay.DIALOGUE
        t.newTypeTab = screenplay.PAREN
        t.nextTypeTab = screenplay.ACTION
        t.prevTypeTab = screenplay.ACTION
        self.types[t.lt] = t

        t = TypeGlobal(screenplay.DIALOGUE)
        t.newTypeEnter = screenplay.CHARACTER
        t.newTypeTab = screenplay.ACTION
        t.nextTypeTab = screenplay.PAREN
        t.prevTypeTab = screenplay.ACTION
        self.types[t.lt] = t

        t = TypeGlobal(screenplay.PAREN)
        t.newTypeEnter = screenplay.DIALOGUE
        t.newTypeTab = screenplay.ACTION
        t.nextTypeTab = screenplay.CHARACTER
        t.prevTypeTab = screenplay.DIALOGUE
        self.types[t.lt] = t

        t = TypeGlobal(screenplay.TRANSITION)
        t.newTypeEnter = screenplay.SCENE
        t.newTypeTab = screenplay.TRANSITION
        t.nextTypeTab = screenplay.SCENE
        t.prevTypeTab = screenplay.CHARACTER
        self.types[t.lt] = t

        t = TypeGlobal(screenplay.SHOT)
        t.newTypeEnter = screenplay.ACTION
        t.newTypeTab = screenplay.CHARACTER
        t.nextTypeTab = screenplay.ACTION
        t.prevTypeTab = screenplay.SCENE
        self.types[t.lt] = t

        t = TypeGlobal(screenplay.ACTBREAK)
        t.newTypeEnter = screenplay.SCENE
        t.newTypeTab = screenplay.ACTION
        t.nextTypeTab = screenplay.SCENE
        t.prevTypeTab = screenplay.SCENE
        self.types[t.lt] = t

        t = TypeGlobal(screenplay.NOTE)
        t.newTypeEnter = screenplay.ACTION
        t.newTypeTab = screenplay.CHARACTER
        t.nextTypeTab = screenplay.ACTION
        t.prevTypeTab = screenplay.CHARACTER
        self.types[t.lt] = t

        # keyboard commands. these must be in alphabetical order.
        self.commands = [] if "TRELBY_TESTING" in os.environ else [
            Command("Abort", "Abort something, e.g. selection,"
                    " auto-completion, etc.", [wx.WXK_ESCAPE], isFixed = True),

            Command("About", "Show the about dialog.", isMenu = True),

            Command("AutoCompletionDlg", "Open the auto-completion dialog.",
                    isMenu = True),

            Command("ChangeToActBreak", "Change current element's style to"
                    " act break.",
                    [util.Key(ord("B"), alt = True).toInt()]),

            Command("ChangeToAction", "Change current element's style to"
                    " action.",
                    [util.Key(ord("A"), alt = True).toInt()]),

            Command("ChangeToCharacter", "Change current element's style to"
                    " character.",
                    [util.Key(ord("C"), alt = True).toInt()]),

            Command("ChangeToDialogue", "Change current element's style to"
                    " dialogue.",
                    [util.Key(ord("D"), alt = True).toInt()]),

            Command("ChangeToNote", "Change current element's style to note.",
                    [util.Key(ord("N"), alt = True).toInt()]),

            Command("ChangeToParenthetical", "Change current element's"
                    " style to parenthetical.",
                    [util.Key(ord("P"), alt = True).toInt()]),

            Command("ChangeToScene", "Change current element's style to"
                    " scene.",
                    [util.Key(ord("S"), alt = True).toInt()]),

            Command("ChangeToShot", "Change current element's style to"
                    " shot."),

            Command("ChangeToTransition", "Change current element's style to"
                    " transition.",
                    [util.Key(ord("T"), alt = True).toInt()]),

            Command("CharacterMap", "Open the character map.",
                    isMenu = True),

            Command("CloseScript", "Close the current script.",
                    [util.Key(23, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("CompareScripts", "Compare two scripts.", isMenu = True),

            Command("Copy", "Copy selected text to the internal clipboard.",
                    [util.Key(3, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("CopySystemCb", "Copy selected text to the system's"
                    " clipboard, unformatted.", isMenu = True),

            Command("CopySystemCbFormatted", "Copy selected text to the system's"
                    " clipboard, formatted.", isMenu = True),

            Command("Cut", "Cut selected text to internal clipboard.",
                    [util.Key(24, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("Delete", "Delete the character under the cursor,"
                    " or selected text.", [wx.WXK_DELETE], isFixed = True),

            Command("DeleteBackward", "Delete the character behind the"
                    " cursor.", [wx.WXK_BACK, util.Key(wx.WXK_BACK, shift = True).toInt()], isFixed = True),

            Command("DeleteElements", "Open the 'Delete elements' dialog.",
                    isMenu = True),

            Command("ExportScript", "Export the current script.",
                    isMenu = True),

            Command("FindAndReplaceDlg", "Open the 'Find & Replace' dialog.",
                    [util.Key(6, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("FindNextError", "Find next error in the current script.",
                    [util.Key(5, ctrl = True).toInt()], isMenu = True),

            Command("ForcedLineBreak", "Insert a forced line break.",
                    [util.Key(wx.WXK_RETURN, ctrl = True).toInt(),
                     util.Key(wx.WXK_RETURN, shift = True).toInt(),

                     # CTRL+Enter under wxMSW
                     util.Key(10, ctrl = True).toInt()],
                    isFixed = True),

            Command("Fullscreen", "Toggle fullscreen.",
                    [util.Key(wx.WXK_F11).toInt()], isFixed = True,
                    isMenu = True),

            Command("GotoPage", "Goto to a given page.",
                    [util.Key(7, ctrl = True).toInt()], isFixed = True,
                    isMenu = True),

            Command("GotoScene", "Goto to a given scene.",
                    [util.Key(ord("G"), alt = True).toInt()], isFixed = True,
                    isMenu = True),

            Command("HeadersDlg", "Open the headers dialog.", isMenu = True),

            Command("HelpCommands", "Show list of commands and their key"
                    " bindings.", isMenu = True),

            Command("HelpManual", "Open the manual.", isMenu = True),

            Command("ImportScript", "Import a script.", isMenu = True),

            Command("InsertNbsp", "Insert non-breaking space.",
                    [util.Key(wx.WXK_SPACE, shift = True, ctrl = True).toInt()],
                    isMenu = True),

            Command("LoadScriptSettings", "Load script-specific settings.",
                    isMenu = True),

            Command("LoadSettings", "Load global settings.", isMenu = True),

            Command("LocationsDlg", "Open the locations dialog.",
                    isMenu = True),

            Command("MoveDown", "Move down.", [wx.WXK_DOWN], isMovement = True,
                    scrollDirection = SCROLL_DOWN),

            Command("MoveEndOfLine", "Move to the end of the line or"
                    " finish auto-completion.",
                    [wx.WXK_END], isMovement = True),

            Command("MoveEndOfScript", "Move to the end of the script.",
                    [util.Key(wx.WXK_END, ctrl = True).toInt()],
                    isMovement = True),

            Command("MoveLeft", "Move left.", [wx.WXK_LEFT], isMovement = True),

            Command("MovePageDown", "Move one page down.",
                    [wx.WXK_PAGEDOWN], isMovement = True),

            Command("MovePageUp", "Move one page up.",
                    [wx.WXK_PAGEUP], isMovement = True),

            Command("MoveRight", "Move right.", [wx.WXK_RIGHT],
                    isMovement = True),

            Command("MoveSceneDown", "Move one scene down.",
                    [util.Key(wx.WXK_DOWN, ctrl = True).toInt()],
                    isMovement = True),

            Command("MoveSceneUp", "Move one scene up.",
                    [util.Key(wx.WXK_UP, ctrl = True).toInt()],
                    isMovement = True),

            Command("MoveStartOfLine", "Move to the start of the line.",
                    [wx.WXK_HOME], isMovement = True),

            Command("MoveStartOfScript", "Move to the start of the"
                    " script.",
                    [util.Key(wx.WXK_HOME, ctrl = True).toInt()],
                    isMovement = True),

            Command("MoveUp", "Move up.", [wx.WXK_UP], isMovement = True,
                    scrollDirection = SCROLL_UP),

            Command("NameDatabase", "Open the character name database.",
                    isMenu = True),

            Command("NewElement", "Create a new element.", [wx.WXK_RETURN],
                    isFixed = True),

            Command("NewScript", "Create a new script.",
                    [util.Key(14, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("OpenScript", "Open a script.",
                    [util.Key(15, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("Paginate", "Paginate current script.", isMenu = True),

            Command("Paste", "Paste text from the internal clipboard.",
                    [util.Key(22, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("PasteSystemCb", "Paste text from the system's"
                    " clipboard.", isMenu = True),

            Command("PrintScript", "Print current script.",
                    [util.Key(16, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("Quit", "Quit the program.",
                    [util.Key(17, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("Redo", "Redo a change that was reverted through undo.",
                    [util.Key(25, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("ReportCharacter", "Generate character report.",
                    isMenu = True),

            Command("ReportDialogueChart", "Generate dialogue chart report.",
                    isMenu = True),

            Command("ReportLocation", "Generate location report.",
                    isMenu = True),

            Command("ReportScene", "Generate scene report.",
                    isMenu = True),

            Command("ReportScript", "Generate script report.",
                    isMenu = True),

            Command("RevertScript", "Revert current script to the"
                    " version on disk.", isMenu = True),

            Command("SaveScript", "Save the current script.",
                    [util.Key(19, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("SaveScriptAs", "Save the current script to a new file.",
                    isMenu = True),

            Command("SaveScriptSettingsAs", "Save script-specific settings"
                    " to a new file.", isMenu = True),

            Command("SaveSettingsAs", "Save global settings to a new file.",
                    isMenu = True),

            Command("ScriptNext", "Change to next open script.",
                    [util.Key(wx.WXK_TAB, ctrl = True).toInt(),
                     util.Key(wx.WXK_PAGEDOWN, ctrl = True).toInt()],
                    isMenu = True),

            Command("ScriptPrev", "Change to previous open script.",
                    [util.Key(wx.WXK_TAB, shift = True, ctrl = True).toInt(),
                     util.Key(wx.WXK_PAGEUP, ctrl = True).toInt()],
                    isMenu = True),

            Command("ScriptSettings", "Change script-specific settings.",
                    isMenu = True),

            Command("SelectAll", "Select the entire script.", isMenu = True),

            Command("SelectScene", "Select the current scene.",
                    [util.Key(1, ctrl = True).toInt()], isMenu = True),

            Command("SetMark", "Set mark at current cursor position.",
                    [util.Key(wx.WXK_SPACE, ctrl = True).toInt()]),

            Command("Settings", "Change global settings.", isMenu = True),

            Command("SpellCheckerDictionaryDlg",
                    "Open the global spell checker dictionary dialog.",
                    isMenu = True),

            Command("SpellCheckerDlg","Spell check the script.",
                    [util.Key(wx.WXK_F8).toInt()], isMenu = True),

            Command("SpellCheckerScriptDictionaryDlg",
                    "Open the script-specific spell checker dictionary"
                    " dialog.",
                    isMenu = True),

            Command("Tab", "Change current element to the next style or"
                    " create a new element.", [wx.WXK_TAB], isFixed = True),

            Command("TabPrev", "Change current element to the previous"
                    " style.",
                    [util.Key(wx.WXK_TAB, shift = True).toInt()],
                    isFixed = True),

            Command("TitlesDlg", "Open the titles dialog.", isMenu = True),

            Command("ToggleShowFormatting", "Toggle 'Show formatting'"
                    " display.", isMenu = True),

            Command("Undo", "Undo the last change.",
                    [util.Key(26, ctrl = True).toInt()],
                    isFixed = True, isMenu = True),

            Command("ViewModeDraft", "Change view mode to draft.",
                    isMenu = True),

            Command("ViewModeLayout", "Change view mode to layout.",
                    isMenu = True),

            Command("ViewModeOverviewLarge", "Change view mode to large"
                    " overview.", isMenu = True),

            Command("ViewModeOverviewSmall", "Change view mode to small"
                    " overview.", isMenu = True),

            Command("ViewModeSideBySide", "Change view mode to side by"
                    " side.", isMenu = True),

            Command("Watermark", "Generate watermarked PDFs.",
                    isMenu = True),
            ]

        self.recalc()

    def setupVars(self):
        v = self.__class__.cvars = mypickle.Vars()

        # how many seconds to show splash screen for on startup (0 = disabled)
        v.addInt("splashTime", 2, "SplashTime", 0, 10)

        # vertical distance between rows, in pixels
        v.addInt("fontYdelta", 18, "FontYDelta", 4, 125)

        # how many lines to scroll per mouse wheel event
        v.addInt("mouseWheelLines", 4, "MouseWheelLines", 1, 50)

        # interval in seconds between automatic pagination (0 = disabled)
        v.addInt("paginateInterval", 1, "PaginateInterval", 0, 10)

        # whether to check script for errors before export / print
        v.addBool("checkOnExport", True, "CheckScriptForErrors")

        # whether to auto-capitalize start of sentences
        v.addBool("capitalize", True, "CapitalizeSentences")

        # whether to auto-capitalize i -> I
        v.addBool("capitalizeI", True, "CapitalizeI")

        # whether to open scripts on their last saved position
        v.addBool("honorSavedPos", True, "OpenScriptOnSavedPos")

        # whether to recenter screen when cursor moves out of it
        v.addBool("recenterOnScroll", False, "RecenterOnScroll")

        # whether to overwrite selected text on typing
        v.addBool("overwriteSelectionOnInsert", True, "OverwriteSelectionOnInsert")

        # whether to use per-elem-type colors (textSceneColor etc.)
        # instead of using textColor for all elem types
        v.addBool("useCustomElemColors", False, "UseCustomElemColors")

        # page break indicators to show
        v.addInt("pbi", PBI_REAL, "PageBreakIndicators", PBI_FIRST,
                    PBI_LAST)

        # PDF viewer program and args. defaults are empty since generating
        # them is a complex process handled by findPDFViewer.
        v.addStrUnicode("pdfViewerPath", u"", "PDF/ViewerPath")
        v.addStrBinary("pdfViewerArgs", "", "PDF/ViewerArguments")

        # fonts. real defaults are set in setDefaultFonts.
        v.addStrBinary("fontNormal", "", "FontNormal")
        v.addStrBinary("fontBold", "", "FontBold")
        v.addStrBinary("fontItalic", "", "FontItalic")
        v.addStrBinary("fontBoldItalic", "", "FontBoldItalic")

        # default script directory
        v.addStrUnicode("scriptDir", misc.progPath, "DefaultScriptDirectory")

        # colors
        v.addColor("text", 0, 0, 0, "TextFG", "Text foreground")
        v.addColor("textHdr", 128, 128, 128, "TextHeadersFG",
                   "Text foreground (headers)")
        v.addColor("textBg", 255, 255, 255, "TextBG", "Text background")
        v.addColor("workspace", 237, 237, 237, "Workspace", "Workspace")
        v.addColor("pageBorder", 202, 202, 202, "PageBorder", "Page border")
        v.addColor("pageShadow", 153, 153, 153, "PageShadow", "Page shadow")
        v.addColor("selected", 200, 200, 200, "Selected", "Selection")
        v.addColor("cursor", 135, 135, 253, "Cursor", "Cursor")
        v.addColor("autoCompFg", 0, 0, 0, "AutoCompletionFG",
                   "Auto-completion foreground")
        v.addColor("autoCompBg", 255, 240, 168, "AutoCompletionBG",
                   "Auto-completion background")
        v.addColor("note", 255, 237, 223, "ScriptNote", "Script note")
        v.addColor("pagebreak", 221, 221, 221, "PageBreakLine",
                   "Page-break line")
        v.addColor("pagebreakNoAdjust", 221, 221, 221,
                   "PageBreakNoAdjustLine",
                   "Page-break (original, not adjusted) line")

        v.addColor("tabText", 50, 50, 50, "TabText", "Tab text")
        v.addColor("tabBorder", 202, 202, 202, "TabBorder",
                   "Tab border")
        v.addColor("tabBarBg", 221, 217, 215, "TabBarBG",
                   "Tab bar background")
        v.addColor("tabNonActiveBg", 180, 180, 180, "TabNonActiveBg", "Tab, non-active")

        for t in getTIs():
            v.addColor("text%s" % t.name, 0, 0, 0, "Text%sFG" % t.name,
                       "Text foreground for %s" % t.name)

        v.makeDicts()

    # 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 = self.cvars.makeVals(s)

        self.cvars.load(vals, "", self)

        for t in self.types.itervalues():
            t.load(vals, "Element/")

        for cmd in self.commands:
            cmd.load(vals, "Command/")

        self.recalc()

    # save config into a string and return that.
    def save(self):
        s = self.cvars.save("", self)

        for t in self.types.itervalues():
            s += t.save("Element/")

        for cmd in self.commands:
            s += cmd.save("Command/")

        return s

    # fix up all invalid config values.
    def recalc(self):
        for it in self.cvars.numeric.itervalues():
            util.clampObj(self, it.name, it.minVal, it.maxVal)

    def getType(self, lt):
        return self.types[lt]

    # add SHIFT+Key alias for all keys bound to movement commands, so
    # selection-movement works.
    def addShiftKeys(self):
        for cmd in self.commands:
            if cmd.isMovement:
                nk = []

                for key in cmd.keys:
                    k = util.Key.fromInt(key)
                    k.shift = True
                    ki = k.toInt()

                    if ki not in cmd.keys:
                        nk.append(ki)

                cmd.keys.extend(nk)

    # remove key (int) from given cmd
    def removeKey(self, cmd, key):
        cmd.keys.remove(key)

        if cmd.isMovement:
            k = util.Key.fromInt(key)
            k.shift = True
            ki = k.toInt()

            if ki in cmd.keys:
                cmd.keys.remove(ki)

    # get textual description of conflicting keys, or None if no
    # conflicts.
    def getConflictingKeys(self):
        keys = {}

        for cmd in self.commands:
            for key in cmd.keys:
                if key in keys:
                    keys[key].append(cmd.name)
                else:
                    keys[key] = [cmd.name]

        s = ""
        for k, v in keys.iteritems():
            if len(v) > 1:
                s += "%s:" % util.Key.fromInt(k).toStr()

                for cmd in v:
                    s += " %s" % cmd

                s += "\n"

        if s == "":
            return None
        else:
            return s

    # set default values that vary depending on platform, wxWidgets
    # version, etc. this is not at the end of __init__ because
    # non-interactive uses have no needs for these.
    def setDefaults(self):
        # check keyboard commands are listed in correct order
        commands = [cmd.name for cmd in self.commands]
        commandsSorted = sorted(commands)

        if commands != commandsSorted:
            # for i in range(len(commands)):
            #     if commands[i] != commandsSorted[i]:
            #         print "Got: %s Expected: %s" % (commands[i], commandsSorted[i])

            # if you get this error, you've put a new command you've added
            # in an incorrect place in the command list. uncomment the
            # above lines to figure out where it should be.
            raise ConfigError("Commands not listed in correct order")

        self.setDefaultFonts()
        self.findPDFViewer()

    # set default fonts
    def setDefaultFonts(self):
        fn = ["", "", "", ""]

        if misc.isUnix:
            fn[0] = "Monospace 12"
            fn[1] = "Monospace Bold 12"
            fn[2] = "Monospace Italic 12"
            fn[3] = "Monospace Bold Italic 12"

        elif misc.isWindows:
                fn[0] = "0;-13;0;0;0;400;0;0;0;0;3;2;1;49;Courier New"
                fn[1] = "0;-13;0;0;0;700;0;0;0;0;3;2;1;49;Courier New"
                fn[2] = "0;-13;0;0;0;400;255;0;0;0;3;2;1;49;Courier New"
                fn[3] = "0;-13;0;0;0;700;255;0;0;0;3;2;1;49;Courier New"

        else:
            raise ConfigError("Unknown platform")

        self.fontNormal = fn[0]
        self.fontBold = fn[1]
        self.fontItalic = fn[2]
        self.fontBoldItalic = fn[3]

    # set PDF viewer program to the best one found on the machine.
    def findPDFViewer(self):
        # list of programs to look for. each item is of the form (name,
        # args). if name is an absolute path only that exact location is
        # looked at, otherwise PATH is searched for the program (on
        # Windows, all paths are interpreted as absolute). args is the
        # list of arguments for the program.
        progs = []

        if misc.isUnix:
            progs = [
                (u"/usr/local/Adobe/Acrobat7.0/bin/acroread", "-tempFile"),
                (u"acroread", "-tempFile"),
                (u"xpdf", ""),
                (u"evince", ""),
                (u"gpdf", ""),
                (u"kpdf", ""),
                (u"okular", ""),
                ]
        elif misc.isWindows:
            # get value via registry if possible, or fallback to old method.
            viewer = util.getWindowsPDFViewer()

            if viewer:
                self.pdfViewerPath = viewer
                self.pdfViewerArgs = ""

                return

            progs = [
                (ur"C:\Program Files\Adobe\Reader 11.0\Reader\AcroRd32.exe",
                 ""),
                (ur"C:\Program Files\Adobe\Reader 10.0\Reader\AcroRd32.exe",
                 ""),
                (ur"C:\Program Files\Adobe\Reader 9.0\Reader\AcroRd32.exe",
                 ""),
                (ur"C:\Program Files\Adobe\Reader 8.0\Reader\AcroRd32.exe",
                 ""),
                (ur"C:\Program Files\Adobe\Acrobat 7.0\Reader\AcroRd32.exe",
                 ""),
                (ur"C:\Program Files\Adobe\Acrobat 6.0\Reader\AcroRd32.exe",
                 ""),
                (ur"C:\Program Files\Adobe\Acrobat 5.0\Reader\AcroRd32.exe",
                 ""),
                (ur"C:\Program Files\Adobe\Acrobat 4.0\Reader\AcroRd32.exe",
                 ""),
                (ur"C:\Program Files\Foxit Software\Foxit Reader\Foxit Reader.exe",
                 ""),
                ]
        else:
            pass

        success = False

        for name, args in progs:
            if misc.isWindows or (name[0] == u"/"):
                if util.fileExists(name):
                    success = True

                    break
            else:
                name = util.findFileInPath(name)

                if name:
                    success = True

                    break

        if success:
            self.pdfViewerPath = name
            self.pdfViewerArgs = args

# config stuff that are wxwindows objects, so can't be in normal
# ConfigGlobal (deepcopy dies)
class ConfigGui:

    # constants
    constantsInited = False
    bluePen = None
    redColor = None
    blackColor = None

    def __init__(self, cfgGl):

        if not ConfigGui.constantsInited:
            ConfigGui.bluePen = wx.Pen(wx.Colour(0, 0, 255))
            ConfigGui.redColor = wx.Colour(255, 0, 0)
            ConfigGui.blackColor = wx.Colour(0, 0, 0)

            ConfigGui.constantsInited = True

        # convert cfgGl.MyColor -> cfgGui.wx.Colour
        for it in cfgGl.cvars.color.itervalues():
            c = getattr(cfgGl, it.name)
            tmp = wx.Colour(c.r, c.g, c.b)
            setattr(self, it.name, tmp)

        # key = line type, value = wx.Colour
        self._lt2textColor = {}

        for t in getTIs():
            self._lt2textColor[t.lt] = getattr(self, "text%sColor" % t.name)

        self.textPen = wx.Pen(self.textColor)
        self.textHdrPen = wx.Pen(self.textHdrColor)

        self.workspaceBrush = wx.Brush(self.workspaceColor)
        self.workspacePen = wx.Pen(self.workspaceColor)

        self.textBgBrush = wx.Brush(self.textBgColor)
        self.textBgPen = wx.Pen(self.textBgColor)

        self.pageBorderPen = wx.Pen(self.pageBorderColor)
        self.pageShadowPen = wx.Pen(self.pageShadowColor)

        self.selectedBrush = wx.Brush(self.selectedColor)
        self.selectedPen = wx.Pen(self.selectedColor)

        self.cursorBrush = wx.Brush(self.cursorColor)
        self.cursorPen = wx.Pen(self.cursorColor)

        self.noteBrush = wx.Brush(self.noteColor)
        self.notePen = wx.Pen(self.noteColor)

        self.autoCompPen = wx.Pen(self.autoCompFgColor)
        self.autoCompBrush = wx.Brush(self.autoCompBgColor)
        self.autoCompRevPen = wx.Pen(self.autoCompBgColor)
        self.autoCompRevBrush = wx.Brush(self.autoCompFgColor)

        self.pagebreakPen = wx.Pen(self.pagebreakColor)
        self.pagebreakNoAdjustPen = wx.Pen(self.pagebreakNoAdjustColor,
                                           style = wx.DOT)

        self.tabTextPen = wx.Pen(self.tabTextColor)
        self.tabBorderPen = wx.Pen(self.tabBorderColor)

        self.tabBarBgBrush = wx.Brush(self.tabBarBgColor)
        self.tabBarBgPen = wx.Pen(self.tabBarBgColor)

        self.tabNonActiveBgBrush = wx.Brush(self.tabNonActiveBgColor)
        self.tabNonActiveBgPen = wx.Pen(self.tabNonActiveBgColor)

        # a 4-item list of FontInfo objects, indexed by the two lowest
        # bits of pml.TextOp.flags.
        self.fonts = []

        for fname in ["fontNormal", "fontBold", "fontItalic",
                      "fontBoldItalic"]:
            fi = FontInfo()

            s = getattr(cfgGl, fname)

            # evil users can set the font name to empty by modifying the
            # config file, and some wxWidgets ports crash hard when trying
            # to create a font from an empty string, so we must guard
            # against that.
            if s:
                nfi = wx.NativeFontInfo()
                nfi.FromString(s)

                fi.font = wx.FontFromNativeInfo(nfi)

                # likewise, evil users can set the font name to "z" or
                # something equally silly, resulting in an
                # invalid/non-existent font. on wxGTK2 and wxMSW we can
                # detect this by checking the point size of the font.
                if fi.font.GetPointSize() == 0:
                    fi.font = None

            # if either of the above failures happened, create a dummy
            # font and use it. this sucks but is preferable to crashing or
            # displaying an empty screen.
            if not fi.font:
                fi.font = wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL,
                                  encoding = wx.FONTENCODING_ISO8859_1)
                setattr(cfgGl, fname, fi.font.GetNativeFontInfo().ToString())

            fx, fy = util.getTextExtent(fi.font, "O")

            fi.fx = max(1, fx)
            fi.fy = max(1, fy)

            self.fonts.append(fi)

    # TextType -> FontInfo
    def tt2fi(self, tt):
        return self.fonts[tt.isBold | (tt.isItalic << 1)]

    # line type -> wx.Colour
    def lt2textColor(self, lt):
        return self._lt2textColor[lt]

def _conv(dict, key, raiseException = True):
    val = dict.get(key)
    if (val == None) and raiseException:
        raise ConfigError("key '%s' not found from '%s'" % (key, dict))

    return val

# get TypeInfos
def getTIs():
    return _ti

def char2lb(char, raiseException = True):
    return _conv(_char2lb, char, raiseException)

def lb2char(lb):
    return _conv(_lb2char, lb)

def lb2str(lb):
    return _conv(_lb2str, lb)

def char2lt(char, raiseException = True):
    ti = _conv(_char2ti, char, raiseException)

    if ti:
        return ti.lt
    else:
        return None

def lt2char(lt):
    return _conv(_lt2ti, lt).char

def name2ti(name, raiseException = True):
    return _conv(_name2ti, name, raiseException)

def lt2ti(lt):
    return _conv(_lt2ti, lt)

def _init():

    for lt, char, name in (
        (screenplay.SCENE,      "\\", "Scene"),
        (screenplay.ACTION,     ".",  "Action"),
        (screenplay.CHARACTER,  "_",  "Character"),
        (screenplay.DIALOGUE,   ":",  "Dialogue"),
        (screenplay.PAREN,      "(",  "Parenthetical"),
        (screenplay.TRANSITION, "/",  "Transition"),
        (screenplay.SHOT,       "=",  "Shot"),
        (screenplay.ACTBREAK,   "@",  "Act break"),
        (screenplay.NOTE,       "%",  "Note")
        ):

        ti = TypeInfo(lt, char, name)

        _ti.append(ti)
        _lt2ti[lt] = ti
        _char2ti[char] = ti
        _name2ti[name] = ti

_init()
GUTIL.PY

from error import *
import misc
import util

import os
import tempfile

if "TRELBY_TESTING" in os.environ:
    import 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 cmp(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:
        wx.EVT_LEFT_DCLICK(btn, 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)
            finally:
                os.close(fd)

            util.showPDF(filename, cfgGl, mainFrame)

        except IOError, (errno, strerror):
            raise MiscError("IOError: %s" % strerror)

    except TrelbyError, e:
        wx.MessageBox("Error writing temporary PDF file: %s" % e,
                      "Error", wx.OK, mainFrame)
MISC.PY

[python]
# -*- coding: iso-8859-1 -*-

import gutil
import opts
import util

import os
import os.path
import sys

if "TRELBY_TESTING" in os.environ:
import mock
wx = mock.Mock()
else:
import wx

TAB_BAR_HEIGHT = 24

version = "2.3-dev"

def init(doWX = True):
global isWindows, isUnix, unicodeFS, doDblBuf, progPath, confPath, tmpPrefix

# prefix used for temp files
tmpPrefix = "trelby-tmp-"

isWindows = False
isUnix = False

if sys.platform.startswith("linux") or sys.platform.startswith("darwin"):
isUnix = True
else:
isWindows = True

# does this platform support using Python's unicode strings in various
# filesystem calls; if not, we need to convert filenames to UTF-8
# before using them.
unicodeFS = isWindows

# wxGTK2 does not need us to do double buffering ourselves, others do
doDblBuf = not isUnix

# stupid hack to keep testcases working, since they don't initialize
# opts (the doWX name is just for similarity with util)
if not doWX or opts.isTest:
progPath = u"."
confPath = u".trelby"
else:
if isUnix:
progPath = unicode(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"UTF-8")

confPath = unicode(os.environ["HOME"], "UTF-8") + u"/.trelby"
else:
progPath = getPathFromRegistry()

confPath = util.getWindowsUnicodeEnvVar(u"USERPROFILE") + ur"\Trelby\conf"

if not os.path.exists(confPath):
os.makedirs(confPath)

def getPathFromRegistry():
registryPath = r"Software\Microsoft\Windows\CurrentVersion\App Paths\trelby.exe"

try:
import _winreg

regPathKey = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, registryPath)
regPathValue, regPathType = _winreg.QueryValueEx(regPathKey, "Path")

if regPathType == _winreg.REG_SZ:
return regPathValue
else:
raise TypeError

except:
wx.MessageBox("There was an error reading the following registry key: %s.\n"
"You may need to reinstall the program to fix this error." %
registryPath, "Error", wx.OK)
sys.exit()

# convert s, which is returned from the wxWidgets GUI and is an Unicode
# string, to a normal string.
def fromGUI(s):
return s.encode("ISO-8859-1", "ignore")

# convert s, which is an Unicode string, to an object suitable for passing
# to Python's file APIs. this is either the Unicode string itself, if the
# platform supports Unicode-based APIs (and Python has implemented support
# for it), or the Unicode string converted to UTF-8 on other platforms.
def toPath(s):
if unicodeFS:
return s
else:
return s.encode("UTF-8")

# return bitmap created from the given file. argument is as for
# getFullPath.
def getBitmap(filename):
return wx.Bitmap(getFullPath(filename))

# return the absolute path of a file under the install dir. so passing in
# "resources/blaa.png" might return "/opt/trelby/resources/blaa.png" for
# example.
def getFullPath(relative):
return progPath + "/" + relative

# TODO: move all GUI stuff to gutil

class MyColorSample(wx.Window):
def __init__(self, parent, id, size):
wx.Window.__init__(self, parent, id, size = size)

wx.EVT_PAINT(self, self.OnPaint)

def OnPaint(self, event):
dc = wx.PaintDC(self)

w, h = self.GetClientSizeTuple()
br = wx.Brush(self.GetBackgroundColour())
dc.SetBrush(br)
dc.DrawRectangle(0, 0, w, h)

# Custom "exit fullscreen" button for our tab bar. Used so that we have
# full control over the button's size.
class MyFSButton(wx.Window):
def __init__(self, parent, id, getCfgGui):
wx.Window.__init__(self, parent, id, size = (TAB_BAR_HEIGHT, TAB_BAR_HEIGHT))

self.getCfgGui = getCfgGui
self.fsImage = getBitmap("resources/fullscreen.png")

wx.EVT_PAINT(self, self.OnPaint)
wx.EVT_LEFT_DOWN(self, self.OnMouseDown)

def OnPaint(self, event):
cfgGui = self.getCfgGui()
dc = wx.PaintDC(self)

w, h = self.GetClientSizeTuple()

dc.SetBrush(cfgGui.tabNonActiveBgBrush)
dc.SetPen(cfgGui.tabBorderPen)
dc.DrawRectangle(0, 0, w, h)

off = (h - self.fsImage.GetHeight()) // 2
dc.DrawBitmap(self.fsImage, off, off)

def OnMouseDown(self, event):
clickEvent = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, self.GetId())
clickEvent.SetEventObject(self)
self.GetEventHandler().ProcessEvent(clickEvent)

# custom status control
class MyStatus(wx.Window):
WIDTH = 280
X_ELEDIVIDER = 100

def __init__(self, parent, id, getCfgGui):
wx.Window.__init__(self, parent, id, size = (MyStatus.WIDTH, TAB_BAR_HEIGHT),
style = wx.FULL_REPAINT_ON_RESIZE)

self.getCfgGui = getCfgGui

self.page = 0
self.pageCnt = 0
self.elemType = ""
self.tabNext = ""
self.enterNext = ""

self.elementFont = util.createPixelFont(
TAB_BAR_HEIGHT // 2 + 6, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.NORMAL)

self.font = util.createPixelFont(
TAB_BAR_HEIGHT // 2 + 2, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.NORMAL)

wx.EVT_PAINT(self, self.OnPaint)

def OnPaint(self, event):
cfgGui = self.getCfgGui()

cy = (TAB_BAR_HEIGHT - 1) // 2
xoff = 5

dc = wx.PaintDC(self)
w, h = self.GetClientSizeTuple()

dc.SetBrush(cfgGui.tabBarBgBrush)
dc.SetPen(cfgGui.tabBarBgPen)
dc.DrawRectangle(0, 0, w, h)

dc.SetPen(cfgGui.tabTextPen)
dc.SetTextForeground(cfgGui.tabTextColor)

pageText = "Page %d / %d" % (self.page, self.pageCnt)
dc.SetFont(self.font)

util.drawText(dc, pageText, MyStatus.WIDTH - xoff, cy,
util.ALIGN_RIGHT, util.VALIGN_CENTER)

s1 = "%s [Enter]" % self.enterNext
s2 = "%s [Tab]" % self.tabNext

x = MyStatus.X_ELEDIVIDER + xoff
dc.DrawText(s1, x, 0)
dc.DrawText(s2, x, cy)

x = xoff
s = "%s" % self.elemType
dc.SetFont(self.elementFont)
util.drawText(dc, s, x, cy, valign = util.VALIGN_CENTER)

dc.SetPen(cfgGui.tabBorderPen)
dc.DrawLine(0, h-1, w, h-1)

for x in (MyStatus.X_ELEDIVIDER, 0):
dc.DrawLine(x, 0, x, h-1)

def SetValues(self, page, pageCnt, elemType, tabNext, enterNext):
self.page = page
self.pageCnt = pageCnt
self.elemType = elemType
self.tabNext = tabNext
self.enterNext = enterNext

self.Refresh(False)


# our own version of a tab control, which exists for two reasons: it does
# not care where it is physically located, which allows us to combine it
# with other controls on a horizontal row, and it consumes less vertical
# space than wx.Notebook. note that this control is divided into two parts,
# MyTabCtrl and MyTabCtrl2, and both must be created.
class MyTabCtrl(wx.Window):
def __init__(self, parent, id, getCfgGui):
style = wx.FULL_REPAINT_ON_RESIZE
wx.Window.__init__(self, parent, id, style = style)

self.getCfgGui = getCfgGui

# pages, i.e., [wx.Window, name] lists. note that 'name' must be an
# Unicode string.
self.pages = []

# index of selected page
self.selected = -1

# index of first visible tab
self.firstTab = 0

# how much padding to leave horizontally at the ends of the
# control, and within each tab
self.paddingX = 10

# starting Y-pos of text in labels
self.textY = 5

# width of a single tab
self.tabWidth = 150

# width, height, spacing, y-pos of arrows
self.arrowWidth = 8
self.arrowHeight = 13
self.arrowSpacing = 3
self.arrowY = 5

# initialized in OnPaint since we don't know our height yet
self.font = None
self.boldFont = None

self.SetMinSize(wx.Size(
self.paddingX * 2 + self.arrowWidth * 2 + self.arrowSpacing +\
self.tabWidth + 5,
TAB_BAR_HEIGHT))

wx.EVT_LEFT_DOWN(self, self.OnLeftDown)
wx.EVT_LEFT_DCLICK(self, self.OnLeftDown)
wx.EVT_SIZE(self, self.OnSize)
wx.EVT_PAINT(self, self.OnPaint)
wx.EVT_ERASE_BACKGROUND(self, self.OnEraseBackground)

# get the ctrl that the tabbed windows should use as a parent
def getTabParent(self):
return self.ctrl2

# get page count
def getPageCount(self):
return len(self.pages)

# get selected page index
def getSelectedPageIndex(self):
return self.selected

# get given page
def getPage(self, i):
return self.pages[i][0]

# MyTabCtrl2 uses this to register itself with us
def add2(self, ctrl2):
self.ctrl2 = ctrl2

# add page
def addPage(self, page, name):
self.pages.append([page, name])

# the new page must be given the correct size and position
self.setPageSizes()
page.MoveXY(0, 0)

self.selectPage(len(self.pages) - 1)

# set all page's sizes
def setPageSizes(self):
size = self.ctrl2.GetClientSize()

for p in self.pages:
p[0].SetClientSizeWH(size.width, size.height)

# select given page
def selectPage(self, page):
self.selected = page

for i in range(len(self.pages)):
w = self.pages[i][0]

if i == self.selected:
w.Show()
else:
w.Hide()

self.pageChangeFunc(self.selected)
self.makeSelectedTabVisible()
self.Refresh(False)

# delete given page
def deletePage(self, i):
self.pages[i][0].Destroy()
del self.pages[i]

self.selectPage(util.clamp(i, 0, len(self.pages) - 1))

# try to change the first visible tag by the given amount.
def scroll(self, delta):
newFirstTab = self.firstTab + delta

if (newFirstTab >= 0) and (newFirstTab < len(self.pages)):
self.firstTab = newFirstTab
self.Refresh(False)

# calculate the maximum number of tabs that we could show with our
# current size.
def calcMaxVisibleTabs(self):
w = self.GetClientSizeTuple()[0]

w -= self.paddingX * 2
w -= self.arrowWidth * 2 + self.arrowSpacing

# leave at least 2 pixels between left arrow and last tab
w -= 2

w //= self.tabWidth

# if by some freak accident we're so small that the above results
# in w being negative or positive but too small, guard against us
# ever returning < 1.
return max(1, w)

# get last visible tab
def getLastVisibleTab(self):
return util.clamp(self.firstTab + self.calcMaxVisibleTabs() - 1,
maxVal = len(self.pages) - 1)

# make sure selected tab is visible
def makeSelectedTabVisible(self):
maxTab = self.getLastVisibleTab()

# if already visible, no need to do anything
if (self.selected >= self.firstTab) and (self.selected <= maxTab):
return

# otherwise, position the selected tab as far right as possible
self.firstTab = util.clamp(
self.selected - self.calcMaxVisibleTabs() + 1,
0)

# set text for tab 'i' to 's'
def setTabText(self, i, s):
self.pages[i][1] = s
self.Refresh(False)

# set function to call when page
Reply
#8
still wanting undo, util, and command for running FindDlg
I also think post misc.py was incomplete as it ends with a commant and no close python tag
Reply
#9
OK, here is misc.py again

MISC.PY

# -*- coding: iso-8859-1 -*-

import gutil
import opts
import util

import os
import os.path
import sys

if "TRELBY_TESTING" in os.environ:
    import mock
    wx = mock.Mock()
else:
    import wx

TAB_BAR_HEIGHT = 24

version = "2.3-dev"

def init(doWX = True):
    global isWindows, isUnix, unicodeFS, doDblBuf, progPath, confPath, tmpPrefix

    # prefix used for temp files
    tmpPrefix = "trelby-tmp-"

    isWindows = False
    isUnix = False

    if sys.platform.startswith("linux") or sys.platform.startswith("darwin"):
        isUnix = True
    else:
        isWindows = True

    # does this platform support using Python's unicode strings in various
    # filesystem calls; if not, we need to convert filenames to UTF-8
    # before using them.
    unicodeFS = isWindows

    # wxGTK2 does not need us to do double buffering ourselves, others do
    doDblBuf = not isUnix

    # stupid hack to keep testcases working, since they don't initialize
    # opts (the doWX name is just for similarity with util)
    if not doWX or opts.isTest:
        progPath = u"."
        confPath = u".trelby"
    else:
        if isUnix:
            progPath = unicode(
                os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
                "UTF-8")

            confPath = unicode(os.environ["HOME"], "UTF-8") + u"/.trelby"
        else:
            progPath = getPathFromRegistry()

            confPath = util.getWindowsUnicodeEnvVar(u"USERPROFILE") + ur"\Trelby\conf"

            if not os.path.exists(confPath):
                os.makedirs(confPath)

def getPathFromRegistry():
    registryPath = r"Software\Microsoft\Windows\CurrentVersion\App Paths\trelby.exe"

    try:
        import _winreg

        regPathKey = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, registryPath)
        regPathValue, regPathType = _winreg.QueryValueEx(regPathKey, "Path")

        if regPathType == _winreg.REG_SZ:
            return regPathValue
        else:
            raise TypeError

    except:
        wx.MessageBox("There was an error reading the following registry key: %s.\n"
                      "You may need to reinstall the program to fix this error." %
                      registryPath, "Error", wx.OK)
        sys.exit()

# convert s, which is returned from the wxWidgets GUI and is an Unicode
# string, to a normal string.
def fromGUI(s):
    return s.encode("ISO-8859-1", "ignore")

# convert s, which is an Unicode string, to an object suitable for passing
# to Python's file APIs. this is either the Unicode string itself, if the
# platform supports Unicode-based APIs (and Python has implemented support
# for it), or the Unicode string converted to UTF-8 on other platforms.
def toPath(s):
    if unicodeFS:
        return s
    else:
        return s.encode("UTF-8")

# return bitmap created from the given file. argument is as for
# getFullPath.
def getBitmap(filename):
    return wx.Bitmap(getFullPath(filename))

# return the absolute path of a file under the install dir. so passing in
# "resources/blaa.png" might return "/opt/trelby/resources/blaa.png" for
# example.
def getFullPath(relative):
    return progPath + "/" + relative

# TODO: move all GUI stuff to gutil

class MyColorSample(wx.Window):
    def __init__(self, parent, id, size):
        wx.Window.__init__(self, parent, id, size = size)

        wx.EVT_PAINT(self, self.OnPaint)

    def OnPaint(self, event):
        dc = wx.PaintDC(self)

        w, h = self.GetClientSizeTuple()
        br = wx.Brush(self.GetBackgroundColour())
        dc.SetBrush(br)
        dc.DrawRectangle(0, 0, w, h)

# Custom "exit fullscreen" button for our tab bar. Used so that we have
# full control over the button's size.
class MyFSButton(wx.Window):
    def __init__(self, parent, id, getCfgGui):
        wx.Window.__init__(self, parent, id, size = (TAB_BAR_HEIGHT, TAB_BAR_HEIGHT))

        self.getCfgGui = getCfgGui
        self.fsImage = getBitmap("resources/fullscreen.png")

        wx.EVT_PAINT(self, self.OnPaint)
        wx.EVT_LEFT_DOWN(self, self.OnMouseDown)

    def OnPaint(self, event):
        cfgGui = self.getCfgGui()
        dc = wx.PaintDC(self)

        w, h = self.GetClientSizeTuple()

        dc.SetBrush(cfgGui.tabNonActiveBgBrush)
        dc.SetPen(cfgGui.tabBorderPen)
        dc.DrawRectangle(0, 0, w, h)

        off = (h - self.fsImage.GetHeight()) // 2
        dc.DrawBitmap(self.fsImage, off, off)

    def OnMouseDown(self, event):
        clickEvent = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, self.GetId())
        clickEvent.SetEventObject(self)
        self.GetEventHandler().ProcessEvent(clickEvent)

# custom status control
class MyStatus(wx.Window):
    WIDTH = 280
    X_ELEDIVIDER = 100

    def __init__(self, parent, id, getCfgGui):
        wx.Window.__init__(self, parent, id, size = (MyStatus.WIDTH, TAB_BAR_HEIGHT),
                           style = wx.FULL_REPAINT_ON_RESIZE)

        self.getCfgGui = getCfgGui

        self.page = 0
        self.pageCnt = 0
        self.elemType = ""
        self.tabNext = ""
        self.enterNext = ""

        self.elementFont = util.createPixelFont(
            TAB_BAR_HEIGHT // 2 + 6, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.NORMAL)

        self.font = util.createPixelFont(
            TAB_BAR_HEIGHT // 2 + 2, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.NORMAL)

        wx.EVT_PAINT(self, self.OnPaint)

    def OnPaint(self, event):
        cfgGui = self.getCfgGui()

        cy = (TAB_BAR_HEIGHT - 1) // 2
        xoff = 5

        dc = wx.PaintDC(self)
        w, h = self.GetClientSizeTuple()

        dc.SetBrush(cfgGui.tabBarBgBrush)
        dc.SetPen(cfgGui.tabBarBgPen)
        dc.DrawRectangle(0, 0, w, h)

        dc.SetPen(cfgGui.tabTextPen)
        dc.SetTextForeground(cfgGui.tabTextColor)

        pageText = "Page %d / %d" % (self.page, self.pageCnt)
        dc.SetFont(self.font)

        util.drawText(dc, pageText, MyStatus.WIDTH - xoff, cy,
            util.ALIGN_RIGHT, util.VALIGN_CENTER)

        s1 = "%s [Enter]" % self.enterNext
        s2 = "%s [Tab]" % self.tabNext

        x = MyStatus.X_ELEDIVIDER + xoff
        dc.DrawText(s1, x, 0)
        dc.DrawText(s2, x, cy)

        x = xoff
        s = "%s" % self.elemType
        dc.SetFont(self.elementFont)
        util.drawText(dc, s, x, cy, valign = util.VALIGN_CENTER)

        dc.SetPen(cfgGui.tabBorderPen)
        dc.DrawLine(0, h-1, w, h-1)

        for x in (MyStatus.X_ELEDIVIDER, 0):
            dc.DrawLine(x, 0, x, h-1)

    def SetValues(self, page, pageCnt, elemType, tabNext, enterNext):
        self.page = page
        self.pageCnt = pageCnt
        self.elemType = elemType
        self.tabNext = tabNext
        self.enterNext = enterNext

        self.Refresh(False)


# our own version of a tab control, which exists for two reasons: it does
# not care where it is physically located, which allows us to combine it
# with other controls on a horizontal row, and it consumes less vertical
# space than wx.Notebook. note that this control is divided into two parts,
# MyTabCtrl and MyTabCtrl2, and both must be created.
class MyTabCtrl(wx.Window):
    def __init__(self, parent, id, getCfgGui):
        style = wx.FULL_REPAINT_ON_RESIZE
        wx.Window.__init__(self, parent, id, style = style)

        self.getCfgGui = getCfgGui

        # pages, i.e., [wx.Window, name] lists. note that 'name' must be an
        # Unicode string.
        self.pages = []

        # index of selected page
        self.selected = -1

        # index of first visible tab
        self.firstTab = 0

        # how much padding to leave horizontally at the ends of the
        # control, and within each tab
        self.paddingX = 10

        # starting Y-pos of text in labels
        self.textY = 5

        # width of a single tab
        self.tabWidth = 150

        # width, height, spacing, y-pos of arrows
        self.arrowWidth = 8
        self.arrowHeight = 13
        self.arrowSpacing = 3
        self.arrowY = 5

        # initialized in OnPaint since we don't know our height yet
        self.font = None
        self.boldFont = None

        self.SetMinSize(wx.Size(
                self.paddingX * 2 + self.arrowWidth * 2 + self.arrowSpacing +\
                    self.tabWidth + 5,
                TAB_BAR_HEIGHT))

        wx.EVT_LEFT_DOWN(self, self.OnLeftDown)
        wx.EVT_LEFT_DCLICK(self, self.OnLeftDown)
        wx.EVT_SIZE(self, self.OnSize)
        wx.EVT_PAINT(self, self.OnPaint)
        wx.EVT_ERASE_BACKGROUND(self, self.OnEraseBackground)

    # get the ctrl that the tabbed windows should use as a parent
    def getTabParent(self):
        return self.ctrl2

    # get page count
    def getPageCount(self):
        return len(self.pages)

    # get selected page index
    def getSelectedPageIndex(self):
        return self.selected

    # get given page
    def getPage(self, i):
        return self.pages[i][0]

    # MyTabCtrl2 uses this to register itself with us
    def add2(self, ctrl2):
        self.ctrl2 = ctrl2

    # add page
    def addPage(self, page, name):
        self.pages.append([page, name])

        # the new page must be given the correct size and position
        self.setPageSizes()
        page.MoveXY(0, 0)

        self.selectPage(len(self.pages) - 1)

    # set all page's sizes
    def setPageSizes(self):
        size = self.ctrl2.GetClientSize()

        for p in self.pages:
            p[0].SetClientSizeWH(size.width, size.height)

    # select given page
    def selectPage(self, page):
        self.selected = page

        for i in range(len(self.pages)):
            w = self.pages[i][0]

            if i == self.selected:
                w.Show()
            else:
                w.Hide()

        self.pageChangeFunc(self.selected)
        self.makeSelectedTabVisible()
        self.Refresh(False)

    # delete given page
    def deletePage(self, i):
        self.pages[i][0].Destroy()
        del self.pages[i]

        self.selectPage(util.clamp(i, 0, len(self.pages) - 1))

    # try to change the first visible tag by the given amount.
    def scroll(self, delta):
        newFirstTab = self.firstTab + delta

        if (newFirstTab >= 0) and (newFirstTab < len(self.pages)):
            self.firstTab = newFirstTab
            self.Refresh(False)

    # calculate the maximum number of tabs that we could show with our
    # current size.
    def calcMaxVisibleTabs(self):
        w = self.GetClientSizeTuple()[0]

        w -= self.paddingX * 2
        w -= self.arrowWidth * 2 + self.arrowSpacing

        # leave at least 2 pixels between left arrow and last tab
        w -= 2

        w //= self.tabWidth

        # if by some freak accident we're so small that the above results
        # in w being negative or positive but too small, guard against us
        # ever returning < 1.
        return max(1, w)

    # get last visible tab
    def getLastVisibleTab(self):
        return util.clamp(self.firstTab + self.calcMaxVisibleTabs() - 1,
                          maxVal = len(self.pages) - 1)

    # make sure selected tab is visible
    def makeSelectedTabVisible(self):
        maxTab = self.getLastVisibleTab()

        # if already visible, no need to do anything
        if (self.selected >= self.firstTab) and (self.selected <= maxTab):
            return

        # otherwise, position the selected tab as far right as possible
        self.firstTab = util.clamp(
            self.selected - self.calcMaxVisibleTabs() + 1,
            0)

    # set text for tab 'i' to 's'
    def setTabText(self, i, s):
        self.pages[i][1] = s
        self.Refresh(False)

    # set function to call when page changes. the function gets a single
    # integer argument, the index of the new page.
    def setPageChangedFunc(self, func):
        self.pageChangeFunc = func

    def OnLeftDown(self, event):
        x = event.GetPosition().x

        if x < self.paddingX:
            return

        w = self.GetClientSizeTuple()[0]

        # start of left arrow
        lx = w - 1 - self.paddingX - self.arrowWidth - self.arrowSpacing \
             - self.arrowWidth + 1

        if x < lx:
            page, pageOffset = divmod(x - self.paddingX, self.tabWidth)
            page += self.firstTab

            if page < len(self.pages):
                hitX = pageOffset >= (self.tabWidth - self.paddingX * 2)

                if hitX:
                    panel = self.pages[page][0]
                    if not panel.ctrl.canBeClosed():
                        return

                    if self.getPageCount() > 1:
                        self.deletePage(page)
                    else:
                        panel.ctrl.createEmptySp()
                        panel.ctrl.updateScreen()
                else:
                    self.selectPage(page)
        else:
            if x < (lx + self.arrowWidth):
                self.scroll(-1)

            # start of right arrow
            rx = lx + self.arrowWidth + self.arrowSpacing

            if (x >= rx) and (x < (rx + self.arrowWidth)) and \
                   (self.getLastVisibleTab() < (len(self.pages) - 1)):
                self.scroll(1)

    def OnSize(self, event):
        size = self.GetClientSize()
        self.screenBuf = wx.EmptyBitmap(size.width, size.height)

    def OnEraseBackground(self, event):
        pass

    def OnPaint(self, event):
        dc = wx.BufferedPaintDC(self, self.screenBuf)

        cfgGui = self.getCfgGui()

        w, h = self.GetClientSizeTuple()

        dc.SetBrush(cfgGui.tabBarBgBrush)
        dc.SetPen(cfgGui.tabBarBgPen)
        dc.DrawRectangle(0, 0, w, h)

        dc.SetPen(cfgGui.tabBorderPen)
        dc.DrawLine(0,h-1,w,h-1)

        xpos = self.paddingX

        tabW = self.tabWidth
        tabH = h - 2
        tabY = h - tabH

        if not self.font:
            textH = h - self.textY - 1
            self.font = util.createPixelFont(
                textH, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.NORMAL)
            self.boldFont = util.createPixelFont(
                textH, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.BOLD)

        maxTab = self.getLastVisibleTab()

        for i in range(self.firstTab, maxTab + 1):
            dc.SetFont(self.font)
            p = self.pages[i]

            dc.DestroyClippingRegion()
            dc.SetClippingRegion(xpos, tabY, tabW, tabH)
            dc.SetPen(cfgGui.tabBorderPen)

            if i == self.selected:
                points=((6,1),(tabW-8,1),(tabW-6,2),(tabW-2,tabH),(0,tabH),(4,2))
                dc.SetBrush(cfgGui.workspaceBrush)
            else:
                points=((5,2),(tabW-8,2),(tabW-6,3),(tabW-2,tabH-1),(0,tabH-1),(3,3))
                dc.SetBrush(cfgGui.tabNonActiveBgBrush)

            dc.DrawPolygon(points,xpos,tabY)

            # clip the text to fit within the tabs
            dc.DestroyClippingRegion()
            dc.SetClippingRegion(xpos, tabY, tabW - self.paddingX * 3, tabH)

            dc.SetPen(cfgGui.tabTextPen)
            dc.SetTextForeground(cfgGui.tabTextColor)
            dc.DrawText(p[1], xpos + self.paddingX, self.textY)

            dc.DestroyClippingRegion()
            dc.SetFont(self.boldFont)
            dc.DrawText("×", xpos + tabW - self.paddingX * 2, self.textY)

            xpos += tabW

        # start of right arrow
        rx = w - 1 - self.paddingX - self.arrowWidth + 1

        if self.firstTab != 0:
            dc.DestroyClippingRegion()
            dc.SetPen(cfgGui.tabTextPen)

            util.drawLine(dc, rx - self.arrowSpacing - 1, self.arrowY,
                          0, self.arrowHeight)
            util.drawLine(dc, rx - self.arrowSpacing - 2, self.arrowY,
                          -self.arrowWidth + 1, self.arrowHeight // 2 + 1)
            util.drawLine(dc, rx - self.arrowSpacing - self.arrowWidth,
                          self.arrowY + self.arrowHeight // 2,
                          self.arrowWidth - 1, self.arrowHeight // 2 + 1)

        if maxTab < (len(self.pages) - 1):
            dc.DestroyClippingRegion()
            dc.SetPen(cfgGui.tabTextPen)

            util.drawLine(dc, rx, self.arrowY, 0, self.arrowHeight)
            util.drawLine(dc, rx + 1, self.arrowY, self.arrowWidth - 1,
                          self.arrowHeight // 2 + 1)
            util.drawLine(dc, rx + 1, self.arrowY + self.arrowHeight - 1,
                          self.arrowWidth - 1, -(self.arrowHeight // 2 + 1))

# second part of MyTabCtrl
class MyTabCtrl2(wx.Window):
    def __init__(self, parent, id, tabCtrl):
        wx.Window.__init__(self, parent, id)

        # MyTabCtrl
        self.tabCtrl = tabCtrl

        self.tabCtrl.add2(self)

        wx.EVT_PAINT(self, self.OnPaint)
        wx.EVT_SIZE(self, self.OnSize)
        wx.EVT_ERASE_BACKGROUND(self, self.OnEraseBackground)

    def OnEraseBackground(self, event):
        pass

    def OnSize(self, event):
        self.tabCtrl.setPageSizes()

    # we have an OnPaint handler that does nothing in a feeble attempt in
    # trying to make sure that in the cases when this does get called, as
    # little (useless) work as possible is done.
    def OnPaint(self, event):
        dc = wx.PaintDC(self)

# dialog that shows two lists of script names, allowing user to choose one
# from both. stores indexes of selections in members named 'sel1' and
# 'sel2' when OK is pressed. 'items' must have at least two items.
class ScriptChooserDlg(wx.Dialog):
    def __init__(self, parent, items):
        wx.Dialog.__init__(self, parent, -1, "Choose scripts",
                           style = wx.DEFAULT_DIALOG_STYLE)

        vsizer = wx.BoxSizer(wx.VERTICAL)

        gsizer = wx.FlexGridSizer(2, 2, 5, 0)

        self.addCombo("first", "Compare script", self, gsizer, items, 0)
        self.addCombo("second", "to", self, gsizer, items, 1)

        vsizer.Add(gsizer)

        self.forceCb = wx.CheckBox(self, -1, "Use same configuration")
        self.forceCb.SetValue(True)
        vsizer.Add(self.forceCb, 0, wx.TOP, 10)

        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)

        wx.EVT_BUTTON(self, cancelBtn.GetId(), self.OnCancel)
        wx.EVT_BUTTON(self, okBtn.GetId(), self.OnOK)

        okBtn.SetFocus()

    def addCombo(self, name, descr, parent, sizer, items, sel):
        al = wx.ALIGN_CENTER_VERTICAL | wx.RIGHT
        if sel == 1:
            al |= wx.ALIGN_RIGHT

        sizer.Add(wx.StaticText(parent, -1, descr), 0, al, 10)

        combo = wx.ComboBox(parent, -1, style = wx.CB_READONLY)
        util.setWH(combo, w = 200)

        for s in items:
            combo.Append(s)

        combo.SetSelection(sel)

        sizer.Add(combo)

        setattr(self, name + "Combo", combo)

    def OnOK(self, event):
        self.sel1 = self.firstCombo.GetSelection()
        self.sel2 = self.secondCombo.GetSelection()
        self.forceSameCfg = bool(self.forceCb.GetValue())

        self.EndModal(wx.ID_OK)

    def OnCancel(self, event):
        self.EndModal(wx.ID_CANCEL)

# CheckBoxDlg below handles lists of these
class CheckBoxItem:
    def __init__(self, text, selected = True, cdata = None):
        self.text = text
        self.selected = selected
        self.cdata = cdata

    # return dict which has keys for all selected items' client data.
    # takes a list of CheckBoxItem's as its parameter. note: this is a
    # static function.
    @staticmethod
    def getClientData(cbil):
        tmp = {}

        for i in range(len(cbil)):
            cbi = cbil[i]

            if cbi.selected:
                tmp[cbi.cdata] = None

        return tmp

# shows one or two (one if cbil2 = None) checklistbox widgets with
# contents from cbil1 and possibly cbil2, which are lists of
# CheckBoxItems. btns[12] are bools for whether or not to include helper
# buttons. if OK is pressed, the incoming lists' items' selection status
# will be modified.
class CheckBoxDlg(wx.Dialog):
    def __init__(self, parent, title, cbil1, descr1, btns1,
                 cbil2 = None, descr2 = None, btns2 = None):
        wx.Dialog.__init__(self, parent, -1, title,
                           style = wx.DEFAULT_DIALOG_STYLE)

        vsizer = wx.BoxSizer(wx.VERTICAL)

        self.cbil1 = cbil1
        self.list1 = self.addList(descr1, self, vsizer, cbil1, btns1, True)

        if cbil2 != None:
            self.cbil2 = cbil2
            self.list2 = self.addList(descr2, self, vsizer, cbil2, btns2,
                                      False, 20)

        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)

        wx.EVT_BUTTON(self, cancelBtn.GetId(), self.OnCancel)
        wx.EVT_BUTTON(self, okBtn.GetId(), self.OnOK)

        okBtn.SetFocus()

    def addList(self, descr, parent, sizer, items, doBtns, isFirst, pad = 0):
        sizer.Add(wx.StaticText(parent, -1, descr), 0, wx.TOP, pad)

        if doBtns:
            hsizer = wx.BoxSizer(wx.HORIZONTAL)

            if isFirst:
                funcs = [ self.OnSet1, self.OnClear1, self.OnToggle1 ]
            else:
                funcs = [ self.OnSet2, self.OnClear2, self.OnToggle2 ]

            tmp = wx.Button(parent, -1, "Set")
            hsizer.Add(tmp)
            wx.EVT_BUTTON(self, tmp.GetId(), funcs[0])

            tmp = wx.Button(parent, -1, "Clear")
            hsizer.Add(tmp, 0, wx.LEFT, 10)
            wx.EVT_BUTTON(self, tmp.GetId(), funcs[1])

            tmp = wx.Button(parent, -1, "Toggle")
            hsizer.Add(tmp, 0, wx.LEFT, 10)
            wx.EVT_BUTTON(self, tmp.GetId(), funcs[2])

            sizer.Add(hsizer, 0, wx.TOP | wx.BOTTOM, 5)

        tmp = wx.CheckListBox(parent, -1)

        longest = -1
        for i in range(len(items)):
            it = items[i]

            tmp.Append(it.text)
            tmp.Check(i, it.selected)

            if isFirst:
                if longest != -1:
                    if len(it.text) > len(items[longest].text):
                        longest = i
                else:
                    longest = 0

        w = -1
        if isFirst:
            h = len(items)
            if longest != -1:
                w = util.getTextExtent(tmp.GetFont(),
                                       "[x] " + items[longest].text)[0] + 15
        else:
            h = min(10, len(items))

        # 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 *= util.getFontHeight(tmp.GetFont()) + 5
        h += 5
        h = max(25, h)

        util.setWH(tmp, w, h)
        sizer.Add(tmp, 0, wx.EXPAND)

        return tmp

    def storeResults(self, cbil, ctrl):
        for i in range(len(cbil)):
            cbil[i].selected = bool(ctrl.IsChecked(i))

    def setAll(self, ctrl, state):
        for i in range(ctrl.GetCount()):
            ctrl.Check(i, state)

    def toggle(self, ctrl):
        for i in range(ctrl.GetCount()):
            ctrl.Check(i, not ctrl.IsChecked(i))

    def OnSet1(self, event):
        self.setAll(self.list1, True)

    def OnClear1(self, event):
        self.setAll(self.list1, False)

    def OnToggle1(self, event):
        self.toggle(self.list1)

    def OnSet2(self, event):
        self.setAll(self.list2, True)

    def OnClear2(self, event):
        self.setAll(self.list2, False)

    def OnToggle2(self, event):
        self.toggle(self.list2)

    def OnOK(self, event):
        self.storeResults(self.cbil1, self.list1)

        if hasattr(self, "list2"):
            self.storeResults(self.cbil2, self.list2)

        self.EndModal(wx.ID_OK)

    def OnCancel(self, event):
        self.EndModal(wx.ID_CANCEL)

# shows a multi-line string to the user in a scrollable text control.
class TextDlg(wx.Dialog):
    def __init__(self, parent, text, title):
        wx.Dialog.__init__(self, parent, -1, title,
                           style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)

        vsizer = wx.BoxSizer(wx.VERTICAL)

        tc = wx.TextCtrl(self, -1, size = wx.Size(400, 200),
                         style = wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_LINEWRAP)
        tc.SetValue(text)
        vsizer.Add(tc, 1, wx.EXPAND);

        vsizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5)

        okBtn = gutil.createStockButton(self, "OK")
        vsizer.Add(okBtn, 0, wx.ALIGN_CENTER)

        util.finishWindow(self, vsizer)

        wx.EVT_BUTTON(self, okBtn.GetId(), self.OnOK)

        okBtn.SetFocus()

    def OnOK(self, event):
        self.EndModal(wx.ID_OK)

# helper function for using TextDlg
def showText(parent, text, title = "Message"):
    dlg = TextDlg(parent, text, title)
    dlg.ShowModal()
    dlg.Destroy()

# ask user for a single-line text input.
class TextInputDlg(wx.Dialog):
    def __init__(self, parent, text, title, validateFunc = None):
        wx.Dialog.__init__(self, parent, -1, title,
                           style = wx.DEFAULT_DIALOG_STYLE | wx.WANTS_CHARS)

        # function to call to validate the input string on OK. can be
        # None, in which case it is not called. if it returns "", the
        # input is valid, otherwise the string it returns is displayed in
        # a message box and the dialog is not closed.
        self.validateFunc = validateFunc

        vsizer = wx.BoxSizer(wx.VERTICAL)

        vsizer.Add(wx.StaticText(self, -1, text), 1, wx.EXPAND | wx.BOTTOM, 5)

        self.tc = wx.TextCtrl(self, -1, style = wx.TE_PROCESS_ENTER)
        vsizer.Add(self.tc, 1, wx.EXPAND);

        vsizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5)

        hsizer = wx.BoxSizer(wx.HORIZONTAL)

        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, 5)

        util.finishWindow(self, vsizer)

        wx.EVT_BUTTON(self, cancelBtn.GetId(), self.OnCancel)
        wx.EVT_BUTTON(self, okBtn.GetId(), self.OnOK)

        wx.EVT_TEXT_ENTER(self, self.tc.GetId(), self.OnOK)

        wx.EVT_CHAR(self.tc, self.OnCharEntry)
        wx.EVT_CHAR(cancelBtn, self.OnCharButton)
        wx.EVT_CHAR(okBtn, self.OnCharButton)

        self.tc.SetFocus()

    def OnCharEntry(self, event):
        self.OnChar(event, True)

    def OnCharButton(self, event):
        self.OnChar(event, False)

    def OnChar(self, event, isEntry):
        kc = event.GetKeyCode()

        if kc == wx.WXK_ESCAPE:
            self.OnCancel()

        elif (kc == wx.WXK_RETURN) and isEntry:
                self.OnOK()

        else:
            event.Skip()

    def OnOK(self, event = None):
        self.input = fromGUI(self.tc.GetValue())

        if self.validateFunc:
            msg = self.validateFunc(self.input)

            if msg:
                wx.MessageBox(msg, "Error", wx.OK, self)

                return

        self.EndModal(wx.ID_OK)

    def OnCancel(self, event = None):
        self.EndModal(wx.ID_CANCEL)

# asks the user for a keypress and stores it.
class KeyDlg(wx.Dialog):
    def __init__(self, parent, cmdName):
        wx.Dialog.__init__(self, parent, -1, "Key capture",
                           style = wx.DEFAULT_DIALOG_STYLE)

        vsizer = wx.BoxSizer(wx.VERTICAL)

        vsizer.Add(wx.StaticText(self, -1, "Press the key combination you\n"
            "want to bind to the command\n'%s'." % cmdName))

        tmp = KeyDlgWidget(self, -1, (1, 1))
        vsizer.Add(tmp)

        util.finishWindow(self, vsizer)

        tmp.SetFocus()

# used by KeyDlg
class KeyDlgWidget(wx.Window):
    def __init__(self, parent, id, size):
        wx.Window.__init__(self, parent, id, size = size,
                           style = wx.WANTS_CHARS)

        wx.EVT_CHAR(self, self.OnKeyChar)

    def OnKeyChar(self, ev):
        p = self.GetParent()
        p.key = util.Key.fromKE(ev)
        p.EndModal(wx.ID_OK)

# handles the "Most recently used" list of files in a menu.
class MRUFiles:
    def __init__(self, maxCount):
        # max number of items
        self.maxCount = maxCount

        # items (Unicode strings)
        self.items = []

        for i in range(self.maxCount):
            id = wx.NewId()

            if i == 0:
                # first menu id
                self.firstId = id
            elif i == (self.maxCount - 1):
                # last menu id
                self.lastId = id

    # use given menu. this must be called before any "add" calls.
    def useMenu(self, menu, menuPos):
        # menu to use
        self.menu = menu

        # position in menu to add first item at
        self.menuPos = menuPos

        # if we already have items, add them to the menu (in reverse order
        # to maintain the correct ordering)
        tmp = self.items
        tmp.reverse()
        self.items = []

        for it in tmp:
            self.add(it)

    # return (firstMenuId, lastMenuId).
    def getIds(self):
        return (self.firstId, self.lastId)

    # add item.
    def add(self, s):
        # remove old menu items
        for i in range(self.getCount()):
            self.menu.Delete(self.firstId + i)

        # if item already exists, remove it
        try:
            i = self.items.index(s)
            del self.items[i]
        except ValueError:
            pass

        # add item to top of list
        self.items.insert(0, s)

        # prune overlong list
        if self.getCount() > self.maxCount:
            self.items = self.items[:self.maxCount]

        # add new menu items
        for i in range(self.getCount()):
            self.menu.Insert(self.menuPos + i, self.firstId + i,
                             "&%d %s" % (
                i + 1, os.path.basename(self.get(i))))

    # return number of items.
    def getCount(self):
        return len(self.items)

    # get item number 'i'.
    def get(self, i):
        return self.items[i]
UNDO.PY

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) = 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, 6)

    if len(linesStrCompressed) < len(linesStr):
        return (len(lines), True, linesStrCompressed)
    else:
        return (len(lines), False, linesStr)

# see lines2storage.
def storage2lines(storage):
    if storage[0] == 0:
        return []

    if storage[1]:
        linesStr = zlib.decompress(storage[2])
    else:
        linesStr = storage[2]

    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 = m2found = 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]:
            m2found = True

            break

        i += 1

    if not l1Big:
        a, c, b, d = a, d, b, c

    return a, c, b, d
UTIL.PY

[python]# -*- coding: utf-8 -*-

from error import *

import datetime
import glob
import gzip
import misc
import os
import re
import tempfile
import time

import StringIO

if "TRELBY_TESTING" in os.environ:
import mock
wx = mock.Mock()
else:
import wx

# alignment values
ALIGN_LEFT = 0
ALIGN_CENTER = 1
ALIGN_RIGHT = 2
VALIGN_TOP = 1
VALIGN_CENTER = 2
VALIGN_BOTTOM = 3

# this has to be below the ALIGN stuff, otherwise things break due to
# circular dependencies
import fontinfo

# mappings from lowercase to uppercase letters for different charsets
_iso_8859_1_map = {
97 : 65, 98 : 66, 99 : 67, 100 : 68, 101 : 69,
102 : 70, 103 : 71, 104 : 72, 105 : 73, 106 : 74,
107 : 75, 108 : 76, 109 : 77, 110 : 78, 111 : 79,
112 : 80, 113 : 81, 114 : 82, 115 : 83, 116 : 84,
117 : 85, 118 : 86, 119 : 87, 120 : 88, 121 : 89,
122 : 90, 224 : 192, 225 : 193, 226 : 194, 227 : 195,
228 : 196, 229 : 197, 230 : 198, 231 : 199, 232 : 200,
233 : 201, 234 : 202, 235 : 203, 236 : 204, 237 : 205,
238 : 206, 239 : 207, 240 : 208, 241 : 209, 242 : 210,
243 : 211, 244 : 212, 245 : 213, 246 : 214, 248 : 216,
249 : 217, 250 : 218, 251 : 219, 252 : 220, 253 : 221,
254 : 222
}

# current mappings, 256 chars long.
_to_upper = ""
_to_lower = ""

# translate table for converting strings to only contain valid input
# characters
_input_tbl = ""

# translate table that converts A-Z -> a-z, keeps a-z as they are, and
# converts everything else to z.
_normalize_tbl = ""

# identity table that maps each character to itself. used by deleteChars.
_identity_tbl = ""

# map some fancy unicode characters to their nearest ASCII/Latin-1
# equivalents so when people import text it's not mangled to uselessness
_fancy_unicode_map = {
ord(u"‘") : u"'",
ord(u"’") : u"'",
ord(u"“") : u'"',
ord(u"”") : u'"',
ord(u"—") : u"--",
ord(u"–") : u"-",
}

# permanent memory DC to get text extents etc
permDc = None

def init(doWX = True):
global _to_upper, _to_lower, _input_tbl, _normalize_tbl, _identity_tbl, \
permDc

# setup ISO-8859-1 case-conversion stuff
tmpUpper = []
tmpLower = []

for i in range(256):
tmpUpper.append(i)
tmpLower.append(i)

for k, v in _iso_8859_1_map.iteritems():
tmpUpper[k] = v
tmpLower[v] = k

for i in range(256):
_to_upper += chr(tmpUpper[i])
_to_lower += chr(tmpLower[i])

# valid input string stuff
for i in range(256):
if isValidInputChar(i):
_input_tbl += chr(i)
else:
_input_tbl += "|"

for i in range(256):
# "a" - "z"
if (i >= 97) and (i <= 122):
ch = chr(i)
# "A" - "Z"
elif (i >= 65) and (i <= 90):
# + 32 ("A" - "a") lowercases it
ch = chr(i + 32)
else:
ch = "z"

_normalize_tbl += ch

_identity_tbl = "".join([chr(i) for i in range(256)])

if doWX:
# dunno if the bitmap needs to be big enough to contain the text
# we're measuring...
permDc = wx.MemoryDC()
permDc.SelectObject(wx.EmptyBitmap(512, 32))

# like string.upper/lower/capitalize, but we do our own charset-handling
# that doesn't need locales etc
def upper(s):
return s.translate(_to_upper)

def lower(s):
return s.translate(_to_lower)

def capitalize(s):
return upper(s[:1]) + s[1:]

# return 's', which must be a unicode string, converted to a ISO-8859-1
# 8-bit string. characters not representable in ISO-8859-1 are discarded.
def toLatin1(s):
return s.encode("ISO-8859-1", "ignore")

# return 's', which must be a string of ISO-8859-1 characters, converted
# to UTF-8.
def toUTF8(s):
return unicode(s, "ISO-8859-1").encode("UTF-8")

# return 's', which must be a string of UTF-8 characters, converted to
# ISO-8859-1, with characters not representable in ISO-8859-1 discarded
# and any invalid UTF-8 sequences ignored.
def fromUTF8(s):
return s.decode("UTF-8", "ignore").encode("ISO-8859-1", "ignore")

# returns True if kc (key-code) is a valid character to add to the script.
def isValidInputChar(kc):
# [0x80, 0x9F] = unspecified control characters in ISO-8859-1, added
# characters like euro etc in windows-1252. 0x7F = backspace, 0xA0 =
# non-breaking space, 0xAD = soft hyphen.
return (kc >= 32) and (kc <= 255) and not\
((kc >= 0x7F) and (kc < 0xA0)) and (kc != 0xAD)

# return s with all non-valid input characters converted to valid input
# characters, except form feeds, which are just deleted.
def toInputStr(s):
return s.translate(_input_tbl, "\f")

# replace fancy unicode characters with their ASCII/Latin1 equivalents.
def removeFancyUnicode(s):
return s.translate(_fancy_unicode_map)

# transform external input (unicode) into a form suitable for having in a
# script
def cleanInput(s):
return toInputStr(toLatin1(removeFancyUnicode(s)))

# replace s[start:start + width] with toInputStr(new) and return s
def replace(s, new, start, width):
return s[0 : start] + toInputStr(new) + s[start + width:]

# delete all characters in 'chars' (a string) from s and return that.
def deleteChars(s, chars):
return s.translate(_identity_tbl, chars)

# returns s with all possible different types of newlines converted to
# unix newlines, i.e. a single "\n"
def fixNL(s):
return s.replace("\r\n", "\n").replace("\r", "\n")

# clamps the given value to a specific range. both limits are optional.
def clamp(val, minVal = None, maxVal = None):
ret = val

if minVal != None:
ret = max(ret, minVal)

if maxVal != None:
ret = min(ret, maxVal)

return ret

# like clamp, but gets/sets value directly from given object
def clampObj(obj, name, minVal = None, maxVal = None):
setattr(obj, name, clamp(getattr(obj, name), minVal, maxVal))

# convert given string to float, clamping it to the given range
# (optional). never throws any exceptions, return defVal (possibly clamped
# as well) on any errors.
def str2float(s, defVal, minVal = None, maxVal = None):
val = defVal

try:
val = float(s)
except (ValueError, OverflowError):
pass

return clamp(val, minVal, maxVal)

# like str2float, but for ints.
def str2int(s, defVal, minVal = None, maxVal = None, radix = 10):
val = defVal

try:
val = int(s, radix)
except ValueError:
pass

return clamp(val, minVal, maxVal)

# extract 'name' field from each item in 'seq', put it in a list, and
# return that list.
def listify(seq, name):
l = []
for it in seq:
l.append(getattr(it, name))

return l

# return percentage of 'val1' of 'val2' (both ints) as an int (50% -> 50
# etc.), or 0 if val2 is 0.
def pct(val1, val2):
if val2 != 0:
return (100 * val1) // val2
else:
return 0

# return percentage of 'val1' of 'val2' (both ints/floats) as a float (50%
# -> 50.0 etc.), or 0.0 if val2 is 0.0
def pctf(val1, val2):
if val2 != 0.0:
return (100.0 * val1) / val2
else:
return 0.0

# return float(val1) / val2, or 0.0 if val2 is 0.0
def safeDiv(val1, val2):
if val2 != 0.0:
return float(val1) / val2
else:
return 0.0

# return float(val1) / val2, or 0.0 if val2 is 0
def safeDivInt(val1, val2):
if val2 != 0:
return float(val1) / val2
else:
return 0.0

# for each character in 'flags', starting at beginning, checks if that
# character is found in 's'. if so, appends True to a tuple, False
# otherwise. returns that tuple, whose length is of course is len(flags).
def flags2bools(s, flags):
b = ()

for f in flags:
if s.find(f) != -1:
b += (True,)
else:
b += (False,)

return b

# reverse of flags2bools. is given a number of objects, if each object
# evaluates to true, chars[i] is appended to the return string. len(chars)
# == len(bools) must be true.
def bools2flags(chars, *bools):
s = ""

if len(chars) != len(bools):
raise TypeError("bools2flags: chars and bools are not equal length")

for i in range(len(chars)):
if bools[i]:
s += chars[i]

return s

# return items, which is a list of ISO-8859-1 strings, as a single string
# with \n between each string. any \ characters in the individual strings
# are escaped as \\.
def escapeStrings(items):
return "\\n".join([s.replace("\\", "\\\\") for s in items])

# opposite of escapeStrings. takes in a string, returns a list of strings.
def unescapeStrings(s):
if not s:
return []

items = []

tmp = ""
i = 0
while i < (len(s) - 1):
ch = s[i]

if ch != "\\":
tmp += ch
i += 1
else:
ch = s[i + 1]

if ch == "n":
items.append(tmp)
tmp = ""
else:
tmp += ch

i += 2

if i < len(s):
tmp += s[i]
items.append(tmp)

return items

# return s encoded so that all characters outside the range [32,126] (and
# "\\") are escaped.
def encodeStr(s):
ret = ""

for ch in s:
c = ord(ch)

# ord("\\") == 92 == 0x5C
if c == 92:
ret += "\\5C"
elif (c >= 32) and (c <= 126):
ret += ch
else:
ret += "\\%02X" % c

return ret

# reverse of encodeStr. if string contains invalid escapes, they're
# silently and arbitrarily replaced by something.
def decodeStr(s):
return re.sub(r"\\..", _decodeRepl, s)

# converts "\A4" style matches to their character values.
def _decodeRepl(mo):
val = str2int(mo.group(0)[1:], 256, 0, 256, 16)

if val != 256:
return chr(val)
else:
return ""

# return string s escaped for use in RTF.
def escapeRTF(s):
return s.replace("\\", "\\\\").replace("{", r"\{").replace("}", r"\}")

# convert mm to twips (1/1440 inch = 1/20 point).
def mm2twips(mm):
# 56.69291 = 1440 / 25.4
return mm * 56.69291

# TODO: move all GUI stuff to gutil

# return True if given font is a fixed-width one.
def isFixedWidth(font):
return getTextExtent(font, "iiiii")[0] == getTextExtent(font, "OOOOO")[0]

# get extent of 's' as (w, h)
def getTextExtent(font, s):
permDc.SetFont(font)

# if we simply return permDc.GetTextExtent(s) from here, on some
# versions of Windows we will incorrectly reject as non-fixed width
# fonts (through isFixedWidth) some fonts that actually are fixed
# width. it's especially bad because one of them is our default font,
# "Courier New".
#
# these are the widths we get for the strings below for Courier New, italic:
#
# iiiii 40
# iiiiiiiiii 80
# OOOOO 41
# OOOOOOOOOO 81
#
# we can see i and O are both 8 pixels wide, so the font is
# fixed-width, but for whatever reason, on the O variants there is one
# additional pixel returned in the width, no matter what the length of
# the string is.
#
# to get around this, we actually call GetTextExtent twice, once with
# the actual string we want to measure, and once with the string
# duplicated, and take the difference between those two as the actual
# width. this handily negates the one-extra-pixel returned and gives
# us an accurate way of checking if a font is fixed width or not.
#
# it's a bit slower but this is not called from anywhere that's
# performance critical.

w1, h = permDc.GetTextExtent(s)
w2 = permDc.GetTextExtent(s + s)[0]

return (w2 - w1, h)

# get height of font in pixels
def getFontHeight(font):
permDc.SetFont(font)
return permDc.GetTextExtent("_\xC5")[1]

# return how many mm tall given font size is.
def getTextHeight(size):
return (size / 72.0) * 25.4

# return how many mm wide given text is at given style with given size.
def getTextWidth(text, style, size):
return (fontinfo.getMetrics(style).getTextWidth(text, size) / 72.0) * 25.4

# create a font that's height is at most 'height' pixels. other parameters
# are the same as in wx.Font's constructor.
def createPixelFont(height, family, style, weight):
fs = 6

selected = fs
closest = 1000
over = 0

# FIXME: what's this "keep trying even once we go over the max height"
# stuff? get rid of it.
while 1:
fn = wx.Font(fs, family, style, weight,
encoding = wx.FONTENCODING_ISO8859_1)
h = getFontHeight(fn)
diff = height -h

if diff >= 0:
if diff < closest:
closest = diff
selected = fs
else:
over += 1

if (over >= 3) or (fs > 144):
break

fs += 2

return wx.Font(selected, family, style, weight,
encoding = wx.FONTENCODING_ISO8859_1)

def reverseComboSelect(combo, clientData):
for i in range(combo.GetCount()):
if combo.GetClientData(i) == clientData:
if combo.GetSelection() != i:
combo.SetSelection(i)

return True

return False

# set widget's client size. if w or h is -1, that dimension is not changed.
def setWH(ctrl, w = -1, h = -1):
size = ctrl.GetClientSize()

if w != -1:
size.width = w

if h != -1:
size.height = h

ctrl.SetMinSize(wx.Size(size.width, size.height))
ctrl.SetClientSizeWH(size.width, size.height)

# wxMSW doesn't respect the control's min/max values at all, so we have to
# implement this ourselves
def getSpinValue(spinCtrl):
tmp = clamp(spinCtrl.GetValue(), spinCtrl.GetMin(), spinCtrl.GetMax())
spinCtrl.SetValue(tmp)

return tmp

# return True if c is not a word character, i.e. is either empty, not an
# alphanumeric character or a "'", or is more than one character.
def isWordBoundary©:
if len© != 1:
return True

if c == "'":
return False

return not isAlnum©

# return True if c is an alphanumeric character
def isAlnum©:
return unicode(c, "ISO-8859-1").isalnum()

# make sure s (unicode) ends in suffix (case-insensitively) and return
# that. suffix must already be lower-case.
def ensureEndsIn(s, suffix):
if s.lower().endswith(suffix):
return s
else:
return s + suffix

# return string 's' split into words (as a list), using isWordBoundary.
def splitToWords(s):
tmp = ""

for c in s:
if isWordBoundary©:
tmp += " "
else:
tmp += c

return tmp.split()

# return two-character prefix of s, using characters a-z only. len(s) must
# be at least 2.
def getWordPrefix(s):
return s[:2].translate(_normalize_tbl)

# return count of how many 'ch' characters 's' begins with.
def countInitial(s, ch):
cnt = 0

for i in range(len(s)):
if s[i] != ch:
break

cnt += 1

return cnt

# searches string 's' for each item of list 'seq', returning True if any
# of them were found.
def multiFind(s, seq):
for it in seq:
if s.find(it) != -1:
return True

return False

# put everything from dictionary d into a list as (key, value) tuples,
# then sort the list and return that. by default sorts by "desc(value)
# asc(key)", but a custom sort function can be given
def sortDict(d, sortFunc = None):
def tmpSortFunc(o1, o2):
ret = cmp(o2[1], o1[1])

if ret != 0:
return ret
else:
return cmp(o1[0], o2[0])

if sortFunc == None:
sortFunc = tmpSortFunc

tmp = []
for k, v in d.iteritems():
tmp.append((k, v))

tmp.sort(sortFunc)

return tmp

# an efficient FIFO container of fixed size. can't contain None objects.
class FIFO:
def __init__(self, size):
self.arr = [None] * size

# index of next slot to fill
self.next = 0

# add item
def add(self, obj):
self.arr[self.next] = obj
self.next += 1

if self.next >= len(self.arr):
self.next = 0

# get contents as a list, in LIFO order.
def get(self):
tmp = []

j = self.next - 1

for i in range(len(self.arr)):
if j < 0:
j = len(self.arr) - 1

obj = self.arr[j]

if obj != None:
tmp.append(obj)

j -= 1

return tmp

# DrawLine-wrapper that makes it easier when the end-point is just
# offsetted from the starting point
def drawLine(dc, x, y, xd, yd):
dc.DrawLine(x, y, x + xd, y + yd)

# draws text aligned somehow. returns a (w, h) tuple of the text extent.
def drawText(dc, text, x, y, align = ALIGN_LEFT, valign = VALIGN_TOP):
w, h = dc.GetTextExtent(text)

if align == ALIGN_CENTER:
x -= w // 2
elif align == ALIGN_RIGHT:
x -= w

if valign == VALIGN_CENTER:
y -= h // 2
elif valign == VALIGN_BOTTOM:
y -= h

dc.DrawText(text, x, y)

return (w, h)

# create pad sizer for given window whose controls are in topSizer, with
# 'pad' pixels of padding on each side, resize window to correct size, and
# optionally center it.
def finishWindow(window, topSizer, pad = 10, center = True):
padSizer = wx.BoxSizer(wx.VERTICAL)
padSizer.Add(topSizer, 1, wx.EXPAND | wx.ALL, pad)
window.SetSizerAndFit(padSizer)
window.Layout()

if center:
window.Center()

# wx.Colour replacement that can safely be copy.deepcopy'd
class MyColor:
def __init__(self, r, g, b):
self.r = r
self.g = g
self.b = b

def toWx(self):
return wx.Colour(self.r, self.g, self.b)

@staticmethod
def fromWx©:
o = MyColor(0, 0, 0)

o.r = c.Red()
o.g = c.Green()
o.b = c.Blue()

return o

# fake key event, supports same operations as the real one
class MyKeyEvent:
def __init__(self, kc = 0):
# keycode
self.kc = kc

self.controlDown = False
self.altDown = False
self.shiftDown = False

def GetKeyCode(self):
return self.kc

def ControlDown(self):
return self.controlDown

def AltDown(self):
return self.altDown

def ShiftDown(self):
return self.shiftDown

def Skip(self):
pass

# one key press
class Key:
keyMap = {
1 : "A",
2 : "B",
3 : "C",
4 : "D",
5 : "E",
6 : "F",
7 : "G",

# CTRL+Enter = 10 in Windows
10 : "Enter (Windows)",

11 : "K",
12 : "L",
14 : "N",
15 : "O",
16 : "P",
17 : "Q",
18 : "R",
19 : "S",
20 : "T",
21 : "U",
22 : "V",
23 : "W",
24 : "X",
25 : "Y",
26 : "Z",
wx.WXK_BACK : "Backspace",
wx.WXK_TAB : "Tab",
wx.WXK_RETURN : "Enter",
wx.WXK_ESCAPE : "Escape",
wx.WXK_DELETE : "Delete",
wx.WXK_END : "End",
wx.WXK_HOME : "Home",
wx.WXK_LEFT : "Left",
wx.WXK_UP : "Up",
wx.WXK_RIGHT : "Right",
wx.WXK_DOWN : "Down",
wx.WXK_PAGEUP : "Page up",
wx.WXK_PAGEDOWN : "Page down",
wx.WXK_INSERT : "Insert",
wx.WXK_F1 : "F1",
wx.WXK_F2 : "F2",
wx.WXK_F3 : "F3",
wx.WXK_F4 : "F4",
wx.WXK_F5 : "F5",
wx.WXK_F6 : "F6",
wx.WXK_F7 : "F7",
wx.WXK_F8 : "F8",
wx.WXK_F9 : "F9",
wx.WXK_F10 : "F10",
wx.WXK_F11 : "F11",
wx.WXK_F12 : "F12",
wx.WXK_F13 : "F13",
wx.WXK_F14 : "F14",
wx.WXK_F15 : "F15",
wx.WXK_F16 : "F16",
wx.WXK_F17 : "F17",
wx.WXK_F18 : "F18",
wx.WXK_F19 : "F19",
wx.WXK_F20 : "F20",
wx.WXK_F21 : "F21",
wx.WXK_F22 : "F22",
wx.WXK_F23 : "F23",
wx.WXK_F24 : "F24",
}

def __init__(self, kc, ctrl = False, alt = False, shift = False):

# we don't want to handle ALT+a/ALT+A etc separately, so uppercase
# input char combinations
if (kc < 256) and (ctrl or alt):
kc = ord(upper(chr(kc)))

# even though the wxWidgets documentation clearly states that
# CTRL+[A-Z] should be returned as keycodes 1-26, wxGTK2 2.6 does
# not do this (wxGTK1 and wxMSG do follow the documentation).
#
# so, we normalize to the wxWidgets official form here if necessary.

# "A" - "Z"
if ctrl and (kc >= 65) and (kc <= 90):
kc -= 64

# ASCII/Latin-1 keycode (0-255) or one of the wx.WXK_ constants (>255)
self.kc = kc

self.ctrl = ctrl
self.alt = alt
self.shift = shift

# returns True if key is a valid input character
def isValidInputChar(self):
return not self.ctrl and not self.alt and isValidInputChar(self.kc)

# toInt/fromInt serialize/deserialize to/from a 35-bit integer, laid
# out like this:
# bits 0-31: keycode
# 32: Control
# 33: Alt
# 34: Shift

def toInt(self):
return (self.kc & 0xFFFFFFFFL) | (self.ctrl << 32L) | \
(self.alt << 33L) | (self.shift << 34L)

@staticmethod
def fromInt(val):
return Key(val & 0xFFFFFFFFL, (val >> 32) & 1, (val >> 33) & 1,
(val >> 34) & 1)

# construct from wx.KeyEvent
@staticmethod
def fromKE(ev):
return Key(ev.GetKeyCode(), ev.ControlDown(), ev.AltDown(),
ev.ShiftDown())

def toStr(self):
s = ""

if self.ctrl:
s += "CTRL+"

if self.alt:
s += "ALT+"

if self.shift:
s += "SHIFT+"

if isValidInputChar(self.kc):
if self.kc == wx.WXK_SPACE:
s += "Space"
else:
s += chr(self.kc)
else:
kname = self.__class__.keyMap.get(self.kc)

if kname:
s += kname
else:
s += "UNKNOWN(%d)" % self.kc

return s

# a string-like object that features reasonably fast repeated appends even
# for large strings, since it keeps each appended string as an item in a
# list.
class String:
def __init__(self, s = None):

# byte count of data appended
self.pos = 0

# list of strings
self.data = []

if s:
self += s

def __len__(self):
return self.pos

def __str__(self):
return "".join(self.data)
Reply
#10
Sorry, but weird things are happening, can't get the files.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Interaction between Matplotlib window, Python prompt and TKinter window NorbertMoussy 3 343 Mar-17-2024, 09:37 AM
Last Post: deanhystad
  [Tkinter] Search data in treeview without search button TomasSanchexx 3 1,527 Aug-12-2023, 03:17 AM
Last Post: deanhystad
  tkinter window and turtle window error 1885 3 6,624 Nov-02-2019, 12:18 PM
Last Post: 1885
  [Tkinter] add search bar = search for input in all computer directory francisco_neves2020 15 10,763 Apr-14-2019, 07:29 PM
Last Post: francisco_neves2020
  update a variable in parent window after closing its toplevel window gray 5 8,976 Mar-20-2017, 10:35 PM
Last Post: Larz60+

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020