# -*- coding: iso-8859-1 -*-
import os
import os.path
import sys
from typing import AnyStr

import trelby.gutil as gutil
import trelby.opts as opts
import trelby.util as util

if "TRELBY_TESTING" in os.environ:
    import unittest.mock as mock

    wx = mock.Mock()
else:
    import wx

TAB_BAR_HEIGHT = 24

version = "2.4.15"


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

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

    isWindows = False
    isUnix = False
    isMac = False

    if sys.platform == "win32":
        isWindows = True
    else:
        isUnix = True
        isMac = sys.platform == "darwin"

    # 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__))))

            default_confPath = str(os.environ["HOME"]) + "/.trelby"

            if os.environ.get("XDG_CONFIG_HOME"):
                confPath = str(os.environ["XDG_CONFIG_HOME"]) + "/trelby"
            elif os.path.exists(default_confPath):
                confPath = default_confPath
            else:
                confPath = str(os.environ["HOME"]) + "/.config/trelby"

        else:
            progPath = str(
                os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
            )  # Removed the registry function because the older installer creator doesn't work anymore and for portable setups it's pointless to setup a check for a registry key

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

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


# 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("trelby/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("x", 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])
