1005 lines
30 KiB
Python
1005 lines
30 KiB
Python
# -*- coding: iso-8859-1 -*-
|
||
|
||
import gutil
|
||
import opts
|
||
import util
|
||
|
||
import os
|
||
import os.path
|
||
import sys
|
||
|
||
if "TRELBY_TESTING" in os.environ:
|
||
import unittest.mock as mock
|
||
wx = mock.Mock()
|
||
else:
|
||
import wx
|
||
|
||
TAB_BAR_HEIGHT = 24
|
||
|
||
version = "2.4.3"
|
||
|
||
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 = "."
|
||
confPath = ".trelby"
|
||
else:
|
||
if isUnix:
|
||
progPath = str(
|
||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
||
confPath = str(os.environ["HOME"]) + "/.trelby"
|
||
else:
|
||
progPath = getPathFromRegistry()
|
||
|
||
confPath = util.getWindowsUnicodeEnvVar("USERPROFILE") + r"\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
|
||
|
||
# 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)
|
||
|
||
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
||
|
||
def OnPaint(self, event):
|
||
dc = wx.PaintDC(self)
|
||
|
||
w, h = self.GetClientSize()
|
||
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")
|
||
|
||
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
||
self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown)
|
||
|
||
def OnPaint(self, event):
|
||
cfgGui = self.getCfgGui()
|
||
dc = wx.PaintDC(self)
|
||
|
||
w, h = self.GetClientSize()
|
||
|
||
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)
|
||
|
||
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
||
|
||
def OnPaint(self, event):
|
||
cfgGui = self.getCfgGui()
|
||
|
||
cy = (TAB_BAR_HEIGHT - 1) // 2
|
||
xoff = 5
|
||
|
||
dc = wx.PaintDC(self)
|
||
w, h = self.GetClientSize()
|
||
|
||
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))
|
||
|
||
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
|
||
self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown)
|
||
self.Bind(wx.EVT_SIZE, self.OnSize)
|
||
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
||
self.Bind(wx.EVT_ERASE_BACKGROUND, 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.Move(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].SetClientSize(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.GetClientSize()[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.GetClientSize()[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.Bitmap(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.GetClientSize()
|
||
|
||
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("<EFBFBD>", 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)
|
||
|
||
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
||
self.Bind(wx.EVT_SIZE, self.OnSize)
|
||
self.Bind(wx.EVT_ERASE_BACKGROUND, 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):
|
||
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)
|
||
|
||
self.Bind(wx.EVT_BUTTON, self.OnCancel, id=cancelBtn.GetId())
|
||
self.Bind(wx.EVT_BUTTON, self.OnOK, id=okBtn.GetId())
|
||
|
||
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)
|
||
|
||
self.Bind(wx.EVT_BUTTON, self.OnCancel, id=cancelBtn.GetId())
|
||
self.Bind(wx.EVT_BUTTON, self.OnOK, id=okBtn.GetId())
|
||
|
||
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)
|
||
self.Bind(wx.EVT_BUTTON, funcs[0], id=tmp.GetId())
|
||
|
||
tmp = wx.Button(parent, -1, "Clear")
|
||
hsizer.Add(tmp, 0, wx.LEFT, 10)
|
||
self.Bind(wx.EVT_BUTTON, funcs[1], id=tmp.GetId())
|
||
|
||
tmp = wx.Button(parent, -1, "Toggle")
|
||
hsizer.Add(tmp, 0, wx.LEFT, 10)
|
||
self.Bind(wx.EVT_BUTTON, funcs[2], id=tmp.GetId())
|
||
|
||
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)
|
||
|
||
self.Bind(wx.EVT_BUTTON, self.OnOK, id=okBtn.GetId())
|
||
|
||
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)
|
||
|
||
self.Bind(wx.EVT_BUTTON, self.OnCancel, id=cancelBtn.GetId())
|
||
self.Bind(wx.EVT_BUTTON, self.OnOK, id=okBtn.GetId())
|
||
|
||
self.Bind(wx.EVT_TEXT_ENTER, self.OnOK, id=self.tc.GetId())
|
||
|
||
self.tc.Bind(wx.EVT_CHAR, self.OnCharEntry)
|
||
cancelBtn.Bind(wx.EVT_CHAR, self.OnCharButton)
|
||
okBtn.Bind(wx.EVT_CHAR, 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)
|
||
|
||
self.Bind(wx.EVT_CHAR, 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, str(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(str(self.get(i)))))
|
||
|
||
# return number of items.
|
||
def getCount(self):
|
||
return len(self.items)
|
||
|
||
# get item number 'i'.
|
||
def get(self, i):
|
||
return str(self.items[i])
|