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)