# -*- coding: iso-8859-1 -*-
from typing import AnyStr

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.9"

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: str) -> AnyStr:
    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_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 OnEraseBackground(self, event):
        pass

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

        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)
        separatorLineThickness = cfgGui.tabBorderPen.GetWidth()
        dc.DrawLine(0,h-separatorLineThickness,w,h-separatorLineThickness)

        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]

            if i == self.selected:
                points=((0,tabH),(4,2),(6,1),(tabW-8,1),(tabW-6,2),(tabW-2,tabH))
                dc.SetBrush(cfgGui.workspaceBrush)
            else:
                points=((0,tabH-separatorLineThickness),(3,3),(5,2),(tabW-8,2),(tabW-6,3),(tabW-2,tabH-separatorLineThickness)) # subtract its thickness to not cover the separator line
                dc.SetBrush(cfgGui.tabNonActiveBgBrush)

            dc.DestroyClippingRegion()

            # draw tab background
            dc.SetClippingRegion(xpos, tabY, tabW, tabH)
            dc.SetPen(wx.Pen(wx.NullPen))
            dc.DrawPolygon(points,xpos,tabY)

            # draw tab borders
            dc.SetClippingRegion(xpos, tabY, tabW, tabH-separatorLineThickness) # tab borders should never cross the seperator line
            dc.SetPen(cfgGui.tabBorderPen)
            dc.DrawLines(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)

        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])
