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
#11
here's the file again.

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

# -*- 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(c):
    if len(c) != 1:
        return True

    if c == "'":
        return False

    return not isAlnum(c)

# return True if c is an alphanumeric character
def isAlnum(c):
    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(c):
            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(c):
        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)

    def __iadd__(self, s):
        s2 = str(s)

        self.data.append(s2)
        self.pos += len(s2)

        return self

# load at most maxSize (all if -1) bytes from 'filename', returning the
# data as a string or None on errors. pops up message boxes with 'frame'
# as parent on errors.
def loadFile(filename, frame, maxSize = -1):
    ret = None

    try:
        f = open(misc.toPath(filename), "rb")

        try:
            ret = f.read(maxSize)
        finally:
            f.close()

    except IOError, (errno, strerror):
        wx.MessageBox("Error loading file '%s': %s" % (
                filename, strerror), "Error", wx.OK, frame)
        ret = None

    return ret

# like loadFile, but if file doesn't exist, tries to load a .gz compressed
# version of it.
def loadMaybeCompressedFile(filename, frame):
    doGz = False

    if not fileExists(filename):
        filename += ".gz"
        doGz = True

    s = loadFile(filename, frame)
    if s is None:
        return None

    if not doGz:
        return s

    buf = StringIO.StringIO(s)

    # python's gzip module throws almost arbitrary exceptions in various
    # error conditions, so the only safe thing to do is to catch
    # everything.
    try:
        f = gzip.GzipFile(mode = "r", fileobj = buf)
        return f.read()
    except:
        wx.MessageBox("Error loading file '%s': Decompression failed" % \
                          filename, "Error", wx.OK, frame)

        return None

# write 'data' to 'filename', popping up a messagebox using 'frame' as
# parent on errors. returns True on success.
def writeToFile(filename, data, frame):
    try:
        f = open(misc.toPath(filename), "wb")

        try:
            f.write(data)
        finally:
            f.close()

        return True

    except IOError, (errno, strerror):
        wx.MessageBox("Error writing file '%s': %s" % (
                filename, strerror), "Error", wx.OK, frame)

        return False

def removeTempFiles(prefix):
    files = glob.glob(tempfile.gettempdir() + "/%s*" % prefix)

    for fn in files:
        try:
            os.remove(fn)
        except OSError:
            continue

# return True if given file exists.
def fileExists(filename):
    try:
        os.stat(misc.toPath(filename))
    except OSError:
        return False

    return True

# look for file 'filename' in all the directories listed in 'dirs', which
# is a list of absolute directory paths. if found, return the absolute
# filename, otherwise None.
def findFile(filename, dirs):
    for d in dirs:
        if d[-1] != u"/":
            d += u"/"

        path = d + filename

        if fileExists(path):
            return path

    return None

# look for file 'filename' in all the directories listed in $PATH. if
# found, return the absolute filename, otherwise None.
def findFileInPath(filename):
    dirs = os.getenv("PATH")
    if not dirs:
        return None

    # I have no idea how one should try to cope if PATH contains entries
    # with non-UTF8 characters, so just ignore any errors
    dirs = unicode(dirs, "UTF-8", "ignore").split(u":")

    # only accept absolute paths. this strips out things like "~/bin/"
    # etc.
    dirs = [d for d in dirs if d and d[0] == u"/"]

    return findFile(filename, dirs)

# simple timer class for use during development only
class TimerDev:

    # how many TimerDev instances are currently in existence
    nestingLevel = 0

    def __init__(self, msg = ""):
        self.msg = msg
        self.__class__.nestingLevel += 1
        self.t = time.time()

    def __del__(self):
        self.t = time.time() - self.t
        self.__class__.nestingLevel -= 1
        print "%s%s took %.5f seconds" % (" " * self.__class__.nestingLevel,
                                          self.msg, self.t)

# Get the Windows default PDF viewer path from registry and return that,
# or None on errors.
def getWindowsPDFViewer():
    try:
        import _winreg

        # HKCR/.pdf: gives the class of the PDF program.
        # Example : AcroRead.Document or FoxitReader.Document

        key = _winreg.OpenKey(_winreg.HKEY_CLASSES_ROOT, ".pdf")
        pdfClass = _winreg.QueryValue(key, "")

        # HKCR/<class>/shell/open/command: the path to the PDF viewer program
        # Example: "C:\Program Files\Acrobat 8.0\acroread.exe" "%1"

        key2 = _winreg.OpenKey(
            _winreg.HKEY_CLASSES_ROOT, pdfClass + r"\shell\open\command")

        # Almost every PDF program out there accepts passing the PDF path
        # as the argument, so we don't parse the arguments from the
        # registry, just get the program path.

        path = _winreg.QueryValue(key2, "").split('"')[1]

        if fileExists(path):
            return path
    except:
        pass

    return None

# get a windows environment variable in its native unicode format, or None
# if not found
def getWindowsUnicodeEnvVar(name):
    import ctypes

    n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0)

    if n == 0:
        return None

    buf = ctypes.create_unicode_buffer(u"\0" * n)
    ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n)

    return buf.value

# show PDF file.
def showPDF(filename, cfgGl, frame):
    def complain():
        wx.MessageBox("PDF viewer application not found.\n\n"
                      "You can change your PDF viewer\n"
                      "settings at File/Settings/Change/Misc.",
                      "Error", wx.OK, frame)

    pdfProgram = cfgGl.pdfViewerPath
    pdfArgs = cfgGl.pdfViewerArgs

    # If configured pdf viewer does not exist, try finding one
    # automatically
    if not fileExists(pdfProgram):
        found = False

        if misc.isWindows:
            regPDF = getWindowsPDFViewer()

            if regPDF:
                wx.MessageBox(
                    "Currently set PDF viewer (%s) was not found.\n"
                    "Change this in File/Settings/Change/Misc.\n\n"
                    "Using the default PDF viewer for Windows instead:\n"
                    "%s" % (pdfProgram, regPDF),
                    "Warning", wx.OK, frame)

                pdfProgram = regPDF
                pdfArgs = ""

                found = True

        if not found:
            complain()

            return

    # on Windows, Acrobat complains about "invalid path" if we
    # give the full path of the program as first arg, so give a
    # dummy arg.
    args = ["pdf"] + pdfArgs.split() + [filename]

    # there's a race condition in checking if the path exists, above, and
    # using it, below. if the file disappears between those two we get an
    # OSError exception from spawnv, so we need to catch it and handle it.

    # TODO: spawnv does not support Unicode paths as of this moment
    # (Python 2.4). for now, convert it to UTF-8 and hope for the best.
    try:
        os.spawnv(os.P_NOWAIT, pdfProgram.encode("UTF-8"), args)
    except OSError:
        complain()
COMMANDSDLG.PY

import misc
import util

import xml.sax.saxutils as xss
import wx
import wx.html

class CommandsDlg(wx.Frame):
    def __init__(self, cfgGl):
        wx.Frame.__init__(self, None, -1, "Commands",
                          size = (650, 600), style = wx.DEFAULT_FRAME_STYLE)

        self.Center()

        vsizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(vsizer)

        s = '<table border="1"><tr><td><b>Key(s)</b></td>'\
            '<td><b>Command</b></td></tr>'

        for cmd in cfgGl.commands:
            s += '<tr><td bgcolor="#dddddd" valign="top">'

            if cmd.keys:
                for key in cmd.keys:
                    k = util.Key.fromInt(key)
                    s += "%s<br>" % xss.escape(k.toStr())
            else:
                s += "No key defined<br>"

            s += '</td><td valign="top">'
            s += "%s" % xss.escape(cmd.desc)
            s += "</td></tr>"

        s += "</table>"

        self.html = """
<html><head></head><body>

%s

<pre>
<b>Mouse:</b>

Left click             Position cursor
Left click + drag      Select text
Right click            Unselect

<b>Keyboard shortcuts in Find/Replace dialog:</b>

F                      Find
R                      Replace
</pre>
</body></html>
        """ % s

        htmlWin = wx.html.HtmlWindow(self)
        rep = htmlWin.GetInternalRepresentation()
        rep.SetIndent(0, wx.html.HTML_INDENT_BOTTOM)
        htmlWin.SetPage(self.html)
        htmlWin.SetFocus()

        vsizer.Add(htmlWin, 1, wx.EXPAND)

        id = wx.NewId()
        menu = wx.Menu()
        menu.Append(id, "&Save as...")

        mb = wx.MenuBar()
        mb.Append(menu, "&File")
        self.SetMenuBar(mb)

        wx.EVT_MENU(self, id, self.OnSave)

        self.Layout()

        wx.EVT_CLOSE(self, self.OnCloseWindow)

    def OnCloseWindow(self, event):
        self.Destroy()

    def OnSave(self, event):
        dlg = wx.FileDialog(self, "Filename to save as",
            wildcard = "HTML files (*.html)|*.html|All files|*",
            style = wx.SAVE | wx.OVERWRITE_PROMPT)

        if dlg.ShowModal() == wx.ID_OK:
            util.writeToFile(dlg.GetPath(), self.html, self)

        dlg.Destroy()
The source code for Trelby can be found at:

https://github.com/oskusalerma/trelby

Or:

https://github.com/oskusalerma/trelby.git

Or:

Trelby.org
Reply
#12
here go the files again:

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

# -*- 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(c):
    if len(c) != 1:
        return True

    if c == "'":
        return False

    return not isAlnum(c)

# return True if c is an alphanumeric character
def isAlnum(c):
    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(c):
            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(c):
        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)

    def __iadd__(self, s):
        s2 = str(s)

        self.data.append(s2)
        self.pos += len(s2)

        return self

# load at most maxSize (all if -1) bytes from 'filename', returning the
# data as a string or None on errors. pops up message boxes with 'frame'
# as parent on errors.
def loadFile(filename, frame, maxSize = -1):
    ret = None

    try:
        f = open(misc.toPath(filename), "rb")

        try:
            ret = f.read(maxSize)
        finally:
            f.close()

    except IOError, (errno, strerror):
        wx.MessageBox("Error loading file '%s': %s" % (
                filename, strerror), "Error", wx.OK, frame)
        ret = None

    return ret

# like loadFile, but if file doesn't exist, tries to load a .gz compressed
# version of it.
def loadMaybeCompressedFile(filename, frame):
    doGz = False

    if not fileExists(filename):
        filename += ".gz"
        doGz = True

    s = loadFile(filename, frame)
    if s is None:
        return None

    if not doGz:
        return s

    buf = StringIO.StringIO(s)

    # python's gzip module throws almost arbitrary exceptions in various
    # error conditions, so the only safe thing to do is to catch
    # everything.
    try:
        f = gzip.GzipFile(mode = "r", fileobj = buf)
        return f.read()
    except:
        wx.MessageBox("Error loading file '%s': Decompression failed" % \
                          filename, "Error", wx.OK, frame)

        return None

# write 'data' to 'filename', popping up a messagebox using 'frame' as
# parent on errors. returns True on success.
def writeToFile(filename, data, frame):
    try:
        f = open(misc.toPath(filename), "wb")

        try:
            f.write(data)
        finally:
            f.close()

        return True

    except IOError, (errno, strerror):
        wx.MessageBox("Error writing file '%s': %s" % (
                filename, strerror), "Error", wx.OK, frame)

        return False

def removeTempFiles(prefix):
    files = glob.glob(tempfile.gettempdir() + "/%s*" % prefix)

    for fn in files:
        try:
            os.remove(fn)
        except OSError:
            continue

# return True if given file exists.
def fileExists(filename):
    try:
        os.stat(misc.toPath(filename))
    except OSError:
        return False

    return True

# look for file 'filename' in all the directories listed in 'dirs', which
# is a list of absolute directory paths. if found, return the absolute
# filename, otherwise None.
def findFile(filename, dirs):
    for d in dirs:
        if d[-1] != u"/":
            d += u"/"

        path = d + filename

        if fileExists(path):
            return path

    return None

# look for file 'filename' in all the directories listed in $PATH. if
# found, return the absolute filename, otherwise None.
def findFileInPath(filename):
    dirs = os.getenv("PATH")
    if not dirs:
        return None

    # I have no idea how one should try to cope if PATH contains entries
    # with non-UTF8 characters, so just ignore any errors
    dirs = unicode(dirs, "UTF-8", "ignore").split(u":")

    # only accept absolute paths. this strips out things like "~/bin/"
    # etc.
    dirs = [d for d in dirs if d and d[0] == u"/"]

    return findFile(filename, dirs)

# simple timer class for use during development only
class TimerDev:

    # how many TimerDev instances are currently in existence
    nestingLevel = 0

    def __init__(self, msg = ""):
        self.msg = msg
        self.__class__.nestingLevel += 1
        self.t = time.time()

    def __del__(self):
        self.t = time.time() - self.t
        self.__class__.nestingLevel -= 1
        print "%s%s took %.5f seconds" % (" " * self.__class__.nestingLevel,
                                          self.msg, self.t)

# Get the Windows default PDF viewer path from registry and return that,
# or None on errors.
def getWindowsPDFViewer():
    try:
        import _winreg

        # HKCR/.pdf: gives the class of the PDF program.
        # Example : AcroRead.Document or FoxitReader.Document

        key = _winreg.OpenKey(_winreg.HKEY_CLASSES_ROOT, ".pdf")
        pdfClass = _winreg.QueryValue(key, "")

        # HKCR/<class>/shell/open/command: the path to the PDF viewer program
        # Example: "C:\Program Files\Acrobat 8.0\acroread.exe" "%1"

        key2 = _winreg.OpenKey(
            _winreg.HKEY_CLASSES_ROOT, pdfClass + r"\shell\open\command")

        # Almost every PDF program out there accepts passing the PDF path
        # as the argument, so we don't parse the arguments from the
        # registry, just get the program path.

        path = _winreg.QueryValue(key2, "").split('"')[1]

        if fileExists(path):
            return path
    except:
        pass

    return None

# get a windows environment variable in its native unicode format, or None
# if not found
def getWindowsUnicodeEnvVar(name):
    import ctypes

    n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0)

    if n == 0:
        return None

    buf = ctypes.create_unicode_buffer(u"\0" * n)
    ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n)

    return buf.value

# show PDF file.
def showPDF(filename, cfgGl, frame):
    def complain():
        wx.MessageBox("PDF viewer application not found.\n\n"
                      "You can change your PDF viewer\n"
                      "settings at File/Settings/Change/Misc.",
                      "Error", wx.OK, frame)

    pdfProgram = cfgGl.pdfViewerPath
    pdfArgs = cfgGl.pdfViewerArgs

    # If configured pdf viewer does not exist, try finding one
    # automatically
    if not fileExists(pdfProgram):
        found = False

        if misc.isWindows:
            regPDF = getWindowsPDFViewer()

            if regPDF:
                wx.MessageBox(
                    "Currently set PDF viewer (%s) was not found.\n"
                    "Change this in File/Settings/Change/Misc.\n\n"
                    "Using the default PDF viewer for Windows instead:\n"
                    "%s" % (pdfProgram, regPDF),
                    "Warning", wx.OK, frame)

                pdfProgram = regPDF
                pdfArgs = ""

                found = True

        if not found:
            complain()

            return

    # on Windows, Acrobat complains about "invalid path" if we
    # give the full path of the program as first arg, so give a
    # dummy arg.
    args = ["pdf"] + pdfArgs.split() + [filename]

    # there's a race condition in checking if the path exists, above, and
    # using it, below. if the file disappears between those two we get an
    # OSError exception from spawnv, so we need to catch it and handle it.

    # TODO: spawnv does not support Unicode paths as of this moment
    # (Python 2.4). for now, convert it to UTF-8 and hope for the best.
    try:
        os.spawnv(os.P_NOWAIT, pdfProgram.encode("UTF-8"), args)
    except OSError:
        complain()
COMMANDSDLG.PY

import misc
import util

import xml.sax.saxutils as xss
import wx
import wx.html

class CommandsDlg(wx.Frame):
    def __init__(self, cfgGl):
        wx.Frame.__init__(self, None, -1, "Commands",
                          size = (650, 600), style = wx.DEFAULT_FRAME_STYLE)

        self.Center()

        vsizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(vsizer)

        s = '<table border="1"><tr><td><b>Key(s)</b></td>'\
            '<td><b>Command</b></td></tr>'

        for cmd in cfgGl.commands:
            s += '<tr><td bgcolor="#dddddd" valign="top">'

            if cmd.keys:
                for key in cmd.keys:
                    k = util.Key.fromInt(key)
                    s += "%s<br>" % xss.escape(k.toStr())
            else:
                s += "No key defined<br>"

            s += '</td><td valign="top">'
            s += "%s" % xss.escape(cmd.desc)
            s += "</td></tr>"

        s += "</table>"

        self.html = """
<html><head></head><body>

%s

<pre>
<b>Mouse:</b>

Left click             Position cursor
Left click + drag      Select text
Right click            Unselect

<b>Keyboard shortcuts in Find/Replace dialog:</b>

F                      Find
R                      Replace
</pre>
</body></html>
        """ % s

        htmlWin = wx.html.HtmlWindow(self)
        rep = htmlWin.GetInternalRepresentation()
        rep.SetIndent(0, wx.html.HTML_INDENT_BOTTOM)
        htmlWin.SetPage(self.html)
        htmlWin.SetFocus()

        vsizer.Add(htmlWin, 1, wx.EXPAND)

        id = wx.NewId()
        menu = wx.Menu()
        menu.Append(id, "&Save as...")

        mb = wx.MenuBar()
        mb.Append(menu, "&File")
        self.SetMenuBar(mb)

        wx.EVT_MENU(self, id, self.OnSave)

        self.Layout()

        wx.EVT_CLOSE(self, self.OnCloseWindow)

    def OnCloseWindow(self, event):
        self.Destroy()

    def OnSave(self, event):
        dlg = wx.FileDialog(self, "Filename to save as",
            wildcard = "HTML files (*.html)|*.html|All files|*",
            style = wx.SAVE | wx.OVERWRITE_PROMPT)

        if dlg.ShowModal() == wx.ID_OK:
            util.writeToFile(dlg.GetPath(), self.html, self)

        dlg.Destroy()
Treby sources and be found at:

trelby.org
or
https://github.com/oskusalerma/trelby
Reply
#13
too many errors to proceed
Reply
#14
you mean it can't be done?
Reply
#15
Quote:you mean it can't be done?
No, Having difficulty to get everything to run.
What version of Python are you running?
Reply
#16
python2 2.7.14-3.0

Trelby depends on:

python2
python-wxgtk
python-crypto
python-lxml

Note:
The official version of Trelby-2.2 runs with python-wxgtk2.8
If you use python-wxgtk3.0, you need to recompile the sources.
If you use Debian or Ubuntu, you can download Trelby-2.3 (which runs with python-wxgtk3.0) here:

http://files.xn--m1agacb.xn--p1acf/programs/trelby/

If you run Arch or Manjaro, you find Trelby-git in AUR.
Reply
#17
Ah, antique python.
Will get to this later today.
Reply
#18
has been suggested that I modify the code this way:

class FindDlg(wx.Dialog):
    def __init__(self, parent, ctrl):
        wx.Dialog.__init__(self, parent, -1, "Find & Replace", pos=(443, 102),
                           style = wx.DEFAULT_DIALOG_STYLE | wx.WANTS_CHARS)


and further down this way:

    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()
commenting out the self center function.
It worked on the install of who suggested it, but not on mine.
Reply
#19
I think the problem lies in the fact that python seems to have cached the finddlg.py file somewhere and uses the cached one even if I edit the one in /trelby/src/.
In fact trelby runs and has the find feature seven if I remove this file.
Have you any idea how to clear the cached files?
Reply
#20
goto: https://searchcode.com/file/98964657/src/trelby.py
and search for cache.
Since I don't know this product, that's the best I can provide
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Interaction between Matplotlib window, Python prompt and TKinter window NorbertMoussy 3 492 Mar-17-2024, 09:37 AM
Last Post: deanhystad
  [Tkinter] Search data in treeview without search button TomasSanchexx 3 1,604 Aug-12-2023, 03:17 AM
Last Post: deanhystad
  tkinter window and turtle window error 1885 3 6,719 Nov-02-2019, 12:18 PM
Last Post: 1885
  [Tkinter] add search bar = search for input in all computer directory francisco_neves2020 15 10,909 Apr-14-2019, 07:29 PM
Last Post: francisco_neves2020
  update a variable in parent window after closing its toplevel window gray 5 9,082 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