#!/usr/bin/python

"""GUIMiner - graphical frontend to Scrypt miners.

Currently supports:
- reaper
- cgminer
- cudaminer

Copyright 2011-2012 Chris MacLeod
Copyright 2012 TacoTime
This program is released under the GNU GPL. See LICENSE.txt for details.
"""

import os, sys, subprocess, errno, re, threading, logging, time, httplib, urllib, distutils.dir_util
print sys.path
import wx
import json
import collections
import pyopencl

from _winreg import (
    CloseKey, OpenKey, QueryValueEx, SetValueEx,
    HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE,
    KEY_ALL_ACCESS, KEY_READ, REG_EXPAND_SZ, REG_SZ
)

"""
Begin startup processes
"""

ON_POSIX = 'posix' in sys.builtin_module_names

try:
    import win32api, win32con, win32process
except ImportError:
    pass

from wx.lib.agw import flatnotebook as fnb
from wx.lib.agw import hyperlink
from wx.lib.newevent import NewEvent

__version__ = 'v0.04'

STARTUP_PATH = os.getcwd()

"""
End startup processes
"""

def get_module_path(): # Redundant with os.getcwd() at opening; not needed?  Tacotime
    """Return the folder containing this script (or its .exe)."""
    module_name = sys.executable if hasattr(sys, 'frozen') else __file__
    abs_path = os.path.abspath(module_name)
    return os.path.dirname(abs_path)

USE_MOCK = '--mock' in sys.argv
# Set up localization; requires the app to be created
app = wx.PySimpleApp(0)
wx.InitAllImageHandlers()

_ = wx.GetTranslation

LANGUAGES = {
    "Chinese Simplified": wx.LANGUAGE_CHINESE_SIMPLIFIED,
    "Dutch": wx.LANGUAGE_DUTCH,
    "English": wx.LANGUAGE_ENGLISH,
    "Esperanto": wx.LANGUAGE_ESPERANTO,
    "French": wx.LANGUAGE_FRENCH,
    "German": wx.LANGUAGE_GERMAN,
    "Hungarian": wx.LANGUAGE_HUNGARIAN,
    "Italian": wx.LANGUAGE_ITALIAN,
    "Portuguese": wx.LANGUAGE_PORTUGUESE,
    "Russian": wx.LANGUAGE_RUSSIAN,
    "Spanish": wx.LANGUAGE_SPANISH,
}
LANGUAGES_REVERSE = dict((v, k) for (k, v) in LANGUAGES.items())

DONATION_ADDRESS = "LiK1rotC2tNYNRbRfW2xsKLYJvKhQ3PwTN"
locale = None
language = None
def update_language(new_language):
    global locale, language
    language = new_language
    if locale:
        del locale

    locale = wx.Locale(language)
    if locale.IsOk():
        locale.AddCatalogLookupPathPrefix(os.path.join(get_module_path(), "locale"))
        locale.AddCatalog("guiminer")
    else:
        locale = None

def load_language():
    language_config = os.path.join(get_module_path(), 'default_language.ini')
    language_data = dict()
    if os.path.exists(language_config):
        with open(language_config) as f:
            language_data.update(json.load(f))
    language_str = language_data.get('language', "English")
    update_language(LANGUAGES.get(language_str, wx.LANGUAGE_ENGLISH))

def save_language():
    language_config = os.path.join(get_module_path(), 'default_language.ini')
    language_str = LANGUAGES_REVERSE.get(language)
    with open(language_config, 'w') as f:
        json.dump(dict(language=language_str), f)

load_language()

ABOUT_TEXT = _(
"""GUIMiner

Version: %(version)s
Scrypt mod by TacoTime
GUI by Chris 'Kiv' MacLeod
Original poclbm miner by m0mchil
Original rpcminer by puddinpop

Get the source code or file issues at GitHub:
    https://github.com/Kiv/poclbm

If you enjoyed this software, support its development
by donating to:

%(address)s

Even a single Litecoin is appreciated and helps motivate
further work on this software.
""")

# Translatable strings that are used repeatedly
STR_NOT_STARTED = _("Not started")
STR_STARTING = _("Starting")
STR_STOPPED = _("Stopped")
STR_PAUSED = _("Paused")
STR_START_MINING = _("Start")
STR_STOP_MINING = _("Stop")
STR_REFRESH_BALANCE = _("Refresh balance")
STR_CONNECTION_ERROR = _("Connection error")
STR_USERNAME = _("Username:")
STR_PASSWORD = _("Password:")
STR_QUIT = _("Quit this program")
STR_ABOUT = _("Show about dialog")

# Alternate backends that we know how to call
SUPPORTED_BACKENDS = [
    "rpcminer-4way.exe",
    "rpcminer-cpu.exe",
    "rpcminer-cuda.exe",
    "rpcminer-opencl.exe",
#    "phoenix.py",
#    "phoenix.exe",
    "bitcoin-miner.exe"
]

USER_AGENT = "guiminer/" + __version__

# Time constants
SAMPLE_TIME_SECS = 3600
REFRESH_RATE_MILLIS = 2000

# Layout constants
LBL_STYLE = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL
BTN_STYLE = wx.ALIGN_CENTER_HORIZONTAL | wx.ALL

# Events sent from the worker threads
(UpdateHashRateEvent, EVT_UPDATE_HASHRATE) = NewEvent()
(UpdateAcceptedEvent, EVT_UPDATE_ACCEPTED) = NewEvent()
(ReaperAttributeUpdate, EVT_REAPER_ATTRIBUTE_UPDATE) = NewEvent()
(UpdateAcceptedReaperEvent, EVT_UPDATE_REAPER_ACCEPTED) = NewEvent()
(UpdateSoloCheckEvent, EVT_UPDATE_SOLOCHECK) = NewEvent()
(UpdateStatusEvent, EVT_UPDATE_STATUS) = NewEvent()

# Used in class CgListenerThread(MinerListenerThread) and ReaperListenerThread(MinerListenerThread)?
non_decimal = re.compile(r'[^\d.]+')

# Utility functions
def merge_whitespace(s):
    """Combine multiple whitespace characters found in s into one."""
    s = re.sub(r"( +)|\t+", " ", s)
    return s.strip()

def get_opencl_devices():
    """Return a list of available OpenCL devices.

    Raises ImportError if OpenCL is not found.
    Raises IOError if no OpenCL devices are found.
    """
    device_strings = []
    platforms = pyopencl.get_platforms() #@UndefinedVariable
    for i, platform in enumerate(platforms):
        devices = platform.get_devices()
        for j, device in enumerate(devices):
            device_strings.append('[%d-%d] %s' % 
                (i, j, merge_whitespace(device.name)[:25]))
    if len(device_strings) == 0:
        raise IOError
    return device_strings

def get_icon_bundle():
    """Return the Bitcoin program icon bundle."""
    return wx.IconBundleFromFile(os.path.join(get_module_path(), "logo.ico"), wx.BITMAP_TYPE_ICO)

def get_taskbar_icon():
    """Return the taskbar icon.

    This works around Window's annoying behavior of ignoring the 16x16 image
    and using nearest neighbour downsampling on the 32x32 image instead."""
    ib = get_icon_bundle()
    return ib.GetIcon((16, 16))

def mkdir_p(path):
    """If the directory 'path' doesn't exist, create it. Same as mkdir -p."""
    try:
        os.makedirs(path)
    except OSError as exc:
        if exc.errno != errno.EEXIST:
            raise

def add_tooltip(widget, text):
    """Add a tooltip to widget with the specified text."""
    tooltip = wx.ToolTip(text)
    widget.SetToolTip(tooltip)

def format_khash(rate):
    """Format rate for display. A rate of 0 means just connected."""
    if rate > 10 ** 6:
        return _("%.3f Ghash/s") % (rate / 1000000.)
    if rate > 10 ** 3:
        return _("%.1f Mhash/s") % (rate / 1000.)
    elif rate == 0:
        return _("Connecting...")
    elif rate == -0.0000001:
        return _("Proxy connected")
    else:
        return _("%d khash/s") % rate

def format_balance(amount):
    """Format a quantity of Bitcoins in BTC."""
    return "%.3f BTC" % float(amount)

def init_logger():
    """Set up and return the logging object and custom formatter."""
    logger = logging.getLogger("poclbm-gui")
    logger.setLevel(logging.DEBUG)
    file_handler = logging.FileHandler(
        os.path.join(get_module_path(), 'guiminer.log'), 'w')
    formatter = logging.Formatter("%(asctime)s: %(message)s",
                                  "%Y-%m-%d %H:%M:%S")
    file_handler.setFormatter(formatter)
    logger.addHandler(file_handler)
    return logger, formatter

logger, formatter = init_logger()

def http_request(hostname, *args, **kwargs):
    """Do a HTTP request and return the response data."""
    conn_cls = httplib.HTTPSConnection if kwargs.get('use_https') else httplib.HTTPConnection        
    conn = conn_cls(hostname) 
    try:        
        logger.debug(_("Requesting balance: %(request)s"), dict(request=args))
        conn.request(*args)
        response = conn.getresponse()
        data = response.read()
        logger.debug(_("Server replied: %(status)s, %(data)s"),
                     dict(status=str(response.status), data=data))
        return response, data
    finally:
        conn.close()

def get_process_affinity(pid):
    """Return the affinity mask for the specified process."""
    flags = win32con.PROCESS_QUERY_INFORMATION
    handle = win32api.OpenProcess(flags, 0, pid)
    return win32process.GetProcessAffinityMask(handle)[0]

def set_process_affinity(pid, mask):
    """Set the affinity for process to mask."""
    flags = win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_SET_INFORMATION
    handle = win32api.OpenProcess(flags, 0, pid)
    win32process.SetProcessAffinityMask(handle, mask)

def find_nth(haystack, needle, n):
    """Return the index of the nth occurrence of needle in haystack."""
    start = haystack.find(needle)
    while start >= 0 and n > 1:
        start = haystack.find(needle, start + len(needle))
        n -= 1
    return start

class ConsolePanel(wx.Panel):
    """Panel that displays logging events.

    Uses with a StreamHandler to log events to a TextCtrl. Thread-safe.
    """
    def __init__(self, parent, n_max_lines):
        wx.Panel.__init__(self, parent, -1)
        self.parent = parent
        self.n_max_lines = n_max_lines

        vbox = wx.BoxSizer(wx.VERTICAL)
        style = wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL
        self.text = wx.TextCtrl(self, -1, "", style=style)
        vbox.Add(self.text, 1, wx.EXPAND)
        self.SetSizer(vbox)

        self.handler = logging.StreamHandler(self)

        formatter = logging.Formatter("%(asctime)s: %(message)s",
                                      "%Y-%m-%d %H:%M:%S")
        self.handler.setFormatter(formatter)
        logger.addHandler(self.handler)

    def on_focus(self):
        """On focus, clear the status bar."""
        self.parent.statusbar.SetStatusText("", 0)
        self.parent.statusbar.SetStatusText("", 1)

    def on_close(self):
        """On closing, stop handling logging events."""
        logger.removeHandler(self.handler)

    def append_text(self, text):
        self.text.AppendText(text)
        lines_to_cut = self.text.GetNumberOfLines() - self.n_max_lines 
        if lines_to_cut > 0:
            contents = self.text.GetValue()
            position = find_nth(contents, '\n', lines_to_cut)
            self.text.ChangeValue(contents[position + 1:])                        

    def write(self, text):
        """Forward logging events to our TextCtrl."""
        wx.CallAfter(self.append_text, text)


class SummaryPanel(wx.Panel):
    """Panel that displays a summary of all miners."""

    def __init__(self, parent):
        wx.Panel.__init__(self, parent, -1)
        self.parent = parent
        self.timer = wx.Timer(self)
        self.timer.Start(REFRESH_RATE_MILLIS)
        self.Bind(wx.EVT_TIMER, self.on_timer)

        flags = wx.ALIGN_CENTER_HORIZONTAL | wx.ALL
        border = 5
        self.column_headers = [
            (wx.StaticText(self, -1, _("Miner")), 0, flags, border),
            (wx.StaticText(self, -1, _("Speed")), 0, flags, border),
            (wx.StaticText(self, -1, _("Accepted")), 0, flags, border),
            (wx.StaticText(self, -1, _("Stale")), 0, flags, border),
            (wx.StaticText(self, -1, _("Start/Stop")), 0, flags, border),
            (wx.StaticText(self, -1, _("Autostart")), 0, flags, border),
        ]
        font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
        font.SetUnderlined(True)
        for st in self.column_headers:
            st[0].SetFont(font)

        self.grid = wx.FlexGridSizer(0, len(self.column_headers), 2, 2)

        self.grid.AddMany(self.column_headers)
        self.add_miners_to_grid()

        self.grid.AddGrowableCol(0)
        self.grid.AddGrowableCol(1)
        self.grid.AddGrowableCol(2)
        self.grid.AddGrowableCol(3)
        self.SetSizer(self.grid)

    def add_miners_to_grid(self):
        """Add a summary row for each miner to the summary grid."""

        # Remove any existing widgets except the column headers.
        for i in reversed(range(len(self.column_headers), len(self.grid.GetChildren()))):
            self.grid.Hide(i)
            self.grid.Remove(i)

        for p in self.parent.profile_panels:
            p.clear_summary_widgets()
            self.grid.AddMany(p.get_summary_widgets(self))

        self.grid.Layout()

    def on_close(self):
        self.timer.Stop()

    def on_timer(self, event=None):
        """Whenever the timer goes off, fefresh the summary data."""
        if self.parent.nb.GetSelection() != self.parent.nb.GetPageIndex(self):
            return

        for p in self.parent.profile_panels:
            p.update_summary()

        self.parent.statusbar.SetStatusText("", 0) # TODO: show something
        total_rate = sum(p.last_rate for p in self.parent.profile_panels
                         if p.is_mining)
        if any(p.is_mining for p in self.parent.profile_panels):
            self.parent.statusbar.SetStatusText(format_khash(total_rate), 1)
        else:
            self.parent.statusbar.SetStatusText("", 1)

    def on_focus(self):
        """On focus, show the statusbar text."""
        self.on_timer()

class GUIMinerTaskBarIcon(wx.TaskBarIcon):
    """Taskbar icon for the GUI.

    Shows status messages on hover and opens on click.
    """
    TBMENU_RESTORE = wx.NewId()
    TBMENU_PAUSE = wx.NewId()
    TBMENU_CLOSE = wx.NewId()
    TBMENU_CHANGE = wx.NewId()
    TBMENU_REMOVE = wx.NewId()

    def __init__(self, frame):
        wx.TaskBarIcon.__init__(self)
        self.frame = frame
        self.icon = get_taskbar_icon()
        self.timer = wx.Timer(self)
        self.timer.Start(REFRESH_RATE_MILLIS)
        self.is_paused = False
        self.SetIcon(self.icon, "GUIMiner")
        self.imgidx = 1
        self.Bind(wx.EVT_TASKBAR_LEFT_DCLICK, self.on_taskbar_activate)
        self.Bind(wx.EVT_MENU, self.on_taskbar_activate, id=self.TBMENU_RESTORE)
        self.Bind(wx.EVT_MENU, self.on_taskbar_close, id=self.TBMENU_CLOSE)
        self.Bind(wx.EVT_MENU, self.on_pause, id=self.TBMENU_PAUSE)
        self.Bind(wx.EVT_TIMER, self.on_timer)

    def CreatePopupMenu(self):
        """Override from wx.TaskBarIcon. Creates the right-click menu."""
        menu = wx.Menu()
        menu.AppendCheckItem(self.TBMENU_PAUSE, _("Pause all"))
        menu.Check(self.TBMENU_PAUSE, self.is_paused)
        menu.Append(self.TBMENU_RESTORE, _("Restore"))
        menu.Append(self.TBMENU_CLOSE, _("Close"))
        return menu

    def on_taskbar_activate(self, evt):
        if self.frame.IsIconized():
            self.frame.Iconize(False)
        if not self.frame.IsShown():
            self.frame.Show(True)
        self.frame.Raise()

    def on_taskbar_close(self, evt):
        wx.CallAfter(self.frame.Close, force=True)

    def on_timer(self, event):
        """Refresh the taskbar icon's status message."""
        objs = self.frame.profile_panels
        if objs:
            text = '\n'.join(p.get_taskbar_text() for p in objs)
            self.SetIcon(self.icon, text)

    def on_pause(self, event):
        """Pause or resume the currently running miners."""
        self.is_paused = event.Checked()
        for miner in self.frame.profile_panels:
            if self.is_paused:
                miner.pause()
            else:
                miner.resume()

def nonBlockRead(output):
    fd = output.fileno()
    fl = fcntl.fcntl(fd, fcntl.F_GETFL)
    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
    try:
        return output.read()
    except:
        return ''
                
class MinerListenerThread(threading.Thread):
    LINES = ["""
        (r"Target =|average rate|Sending to server|found hash|connected to|Setting server",
            lambda _: None), # Just ignore lines like these
        (r"accepted|\"result\":\s*true",
            lambda _: UpdateAcceptedEvent(accepted=True)),
        (r"invalid|stale|rejected", lambda _:
            UpdateAcceptedEvent(accepted=False)),     
        (r"(\d+)\s*Kh/s", lambda match:
            UpdateHashRateEvent(rate=float(match.group(1)))),
        (r"(\d+\.\d+)\s*MH/s", lambda match:
            UpdateHashRateEvent(rate=float(match.group(1)) * 1000)),
        (r"(\d+\.\d+)\s*Mhash/s", lambda match:
            UpdateHashRateEvent(rate=float(match.group(1)) * 1000)),
        (r"(\d+)\s*Mhash/s", lambda match:
            UpdateHashRateEvent(rate=int(match.group(1)) * 1000)),
        (r"checking (\d+)", lambda _:
            UpdateSoloCheckEvent()),"""
    ] 

    def __init__(self, parent, miner):
        threading.Thread.__init__(self)
        self.shutdown_event = threading.Event()
        self.parent = parent
        self.parent_name = parent.name
        self.miner = miner
        
    def run(self):
        logger.info(_('Listener for "%s" started') % self.parent_name)
        while not self.shutdown_event.is_set():
            line = self.miner.stdout.readline().strip()
            # logger.debug("Line: %s", line)
            if not line: continue
           
            for s, event_func in self.LINES: # Use self to allow subclassing
                match = re.search(s, line, flags=re.I)
                if match is not None:
                    event = event_func(match)
                    if event is not None:
                        wx.PostEvent(self.parent, event)
                    break     
           
            else:
                # Possible error or new message, just pipe it through
                event = UpdateStatusEvent(text=line)
                logger.info(_('Listener for "%(name)s": %(line)s'),
                            dict(name=self.parent_name, line=line))
                wx.PostEvent(self.parent, event)
        logger.info(_('Listener for "%s" shutting down'), self.parent_name)
    

class PhoenixListenerThread(MinerListenerThread):
    LINES = [
        (r"Result: .* accepted",
            lambda _: UpdateAcceptedEvent(accepted=True)),
        (r"Result: .* rejected", lambda _:
            UpdateAcceptedEvent(accepted=False)),
        (r"(\d+)\.?(\d*) Khash/sec", lambda match:
            UpdateHashRateEvent(rate=float(match.group(1) + '.' + match.group(2)))),
        (r"(\d+)\.?(\d*) Mhash/sec", lambda match:
            UpdateHashRateEvent(rate=float(match.group(1) + '.' + match.group(2)) * 1000)),
        (r"Currently on block",
            lambda _: None), # Just ignore lines like these
    ]
    
class CgListenerThread(MinerListenerThread):
    LINES = [
        (r" Accepted .*",
            lambda _: UpdateAcceptedEvent(accepted=True)),
        #(r"A:*\d+",
        #    lambda _: UpdateAcceptedEvent(accepted=False)),
        (r" Rejected .*",
            lambda _: UpdateAcceptedEvent(accepted=False)),
        #(r"R:*\d+",
        #    lambda _: UpdateAcceptedEvent(accepted=False)),
        #(r"Q:*\d+",
        #    lambda _: UpdateAcceptedEvent(accepted=False)),
        #(r"HW:*\d+",
        #    lambda _: UpdateAcceptedEvent(accepted=False)),
        #(r"\(\d+s\):(\d+)\.?(\d*) .* Kh/s", lambda match:
        #    UpdateHashRateEvent(rate=float(match.group(1) + '.' + match.group(2)) * 1000)),
        (r"\(*avg\):.*Kh", lambda match:
            UpdateHashRateEvent(rate=float(non_decimal.sub('', match.group(0))))),
        (r"\(*avg\):.*Mh", lambda match:
            UpdateHashRateEvent(rate=float(non_decimal.sub('', match.group(0))) * 1000)),
        (r"^GPU\s*\d+",
            lambda _: None), # Just ignore lines like these
    ]

# Below is kind of an ugly hack for updating reaper shares, but it works - TacoTime
class ReaperListenerThread(MinerListenerThread):
    LINES = [
        (r"GPU \d+.*", lambda match:
            ReaperAttributeUpdate(clstring=match.group(0)))
    ]
     
class CudaminerListenerThread(MinerListenerThread):
    LINES = [
        (r"(yay!!!)",
            lambda _: UpdateAcceptedEvent(accepted=True)),
        (r"(booooo)",
            lambda _: UpdateAcceptedEvent(accepted=False)),
        (r"\hashes, .*khash", lambda match:
            UpdateHashRateEvent(rate=float(non_decimal.sub('', match.group(0))))),
        #(r"^GPU\s*\d+",
        #    lambda _: None), # Just ignore lines like these
    ]
     
class ProxyListenerThread(MinerListenerThread):
    LINES = [
        (r".* accepted, .*",
            lambda _: UpdateAcceptedEvent(accepted=True)),
        (r".* REJECTED:.*",
            lambda _: UpdateAcceptedEvent(accepted=False)),
        (r".*LISTENING.*", lambda match:
            UpdateHashRateEvent(rate = -0.0000001)),
    ]

class MinerTab(wx.Panel):
    """A tab in the GUI representing a miner instance.

    Each MinerTab has these responsibilities:
    - Persist its data to and from the config file
    - Launch a backend subprocess and monitor its progress
      by creating a MinerListenerThread.
    - Post updates to the GUI's statusbar & summary panel; the format depends
      whether the backend is working solo or in a pool.
    """
    def __init__(self, parent, id, devices, servers, defaults, gpusettings_data, statusbar, data):
        wx.Panel.__init__(self, parent, id)
        self.parent = parent
        self.servers = servers
        self.defaults = defaults
        self.gpusettings_data = gpusettings_data
        self.statusbar = statusbar
        self.is_mining = False
        self.is_paused = False
        self.is_possible_error = False
        self.miner = None # subprocess.Popen instance when mining
        self.miner_listener = None # MinerListenerThread when mining
        self.solo_blocks_found = 0
        self.accepted_shares = 0 # shares for pool, diff1 hashes for solo
        self.accepted_times = collections.deque()
        self.invalid_shares = 0
        self.invalid_times = collections.deque()
        self.last_rate = 0 # units of khash/s
        self.autostart = False
        self.num_processors = int(os.getenv('NUMBER_OF_PROCESSORS', 1))
        self.affinity_mask = 0
        self.server_lbl = wx.StaticText(self, -1, _("Server:"))
        self.summary_panel = None # SummaryPanel instance if summary open
        self.server = wx.ComboBox(self, -1,
                                  choices=[s['name'] for s in servers],
                                  style=wx.CB_READONLY)
        self.gpusettings_lbl = wx.StaticText(self, -1, _("GPU Defaults:"))
        self.gpusettings = wx.ComboBox(self, -1,
                                  choices=[s['name'] for s in gpusettings_data],
                                  style=wx.CB_READONLY)
        self.website_lbl = wx.StaticText(self, -1, _("Website:"))
        self.website = hyperlink.HyperLinkCtrl(self, -1, "")
        self.external_lbl = wx.StaticText(self, -1, _("Ext. Path:"))
        self.txt_external = wx.TextCtrl(self, -1, "")
        self.host_lbl = wx.StaticText(self, -1, _("Host:"))
        self.txt_host = wx.TextCtrl(self, -1, "")
        self.port_lbl = wx.StaticText(self, -1, _("Port:"))
        self.txt_port = wx.TextCtrl(self, -1, "")
        self.user_lbl = wx.StaticText(self, -1, STR_USERNAME)
        self.txt_username = wx.TextCtrl(self, -1, "")
        self.pass_lbl = wx.StaticText(self, -1, STR_PASSWORD)
        self.txt_pass = wx.TextCtrl(self, -1, "", style=wx.TE_PASSWORD)
        self.device_lbl = wx.StaticText(self, -1, _("Device:"))
        self.device_listbox = wx.ComboBox(self, -1, choices=devices or [_("No OpenCL devices")], style=wx.CB_READONLY)
        self.minercgminer_lbl = wx.StaticText(self, -1, _("Miner: cgminer"))
        self.minerreaper_lbl = wx.StaticText(self, -1, _("Miner: reaper"))
        self.minercudaminer_lbl = wx.StaticText(self, -1, _("Miner: cudaminer"))
        self.proxy_lbl = wx.StaticText(self, -1, _("Stratum proxy"))
        self.thrcon_lbl = wx.StaticText(self, -1, _("Thread concurrency:"))
        self.txt_thrcon = wx.TextCtrl(self, -1, "")
        self.worksize_lbl = wx.StaticText(self, -1, _("Worksize:"))
        self.txt_worksize = wx.TextCtrl(self, -1, "")
        self.vectors_lbl = wx.StaticText(self, -1, _("Vectors:"))
        self.txt_vectors = wx.TextCtrl(self, -1, "")
        self.intensity_lbl = wx.StaticText(self, -1, _("Intensity:"))
        self.txt_intensity = wx.TextCtrl(self, -1, "")
        self.gputhreads_lbl = wx.StaticText(self, -1, _("GPU threads:"))
        self.txt_gputhreads = wx.TextCtrl(self, -1, "")
        self.flags_lbl = wx.StaticText(self, -1, _("Extra flags:"))
        self.txt_flags = wx.TextCtrl(self, -1, "")
        self.extra_info = wx.StaticText(self, -1, "")
        self.affinity_lbl = wx.StaticText(self, -1, _("CPU Affinity:"))
        #self.affinity_chks = [wx.CheckBox(self, label='%d ' % i)
        #                      for i in range(self.num_processors)]
        self.stratum_lbl = wx.StaticText(self, -1, _("Use stratum:"))
        self.txt_stratum = wx.ComboBox(self, -1,
                                  choices=['Yes','No'],
                                  style=wx.CB_READONLY)
        self.interactive_lbl = wx.StaticText(self, -1, _("Interactive:"))
        self.txt_interactive = wx.ComboBox(self, -1,
                                  choices=['Yes','No'],
                                  style=wx.CB_READONLY)
        self.texcache_lbl = wx.StaticText(self, -1, _("Texture cache:"))
        self.txt_texcache = wx.ComboBox(self, -1,
                                  choices=['Disabled','1D','2D'],
                                  style=wx.CB_READONLY)
        self.singlemem_lbl = wx.StaticText(self, -1, _("Multiblock memory:"))
        self.txt_singlemem = wx.ComboBox(self, -1,
                                  choices=['Yes','No'],
                                  style=wx.CB_READONLY)
        self.warpflags_lbl = wx.StaticText(self, -1, _("Warp configuration:"))
        self.txt_warpflags = wx.TextCtrl(self, -1, "")
        self.stratuminfo0_lbl = wx.StaticText(self, -1, _("Connect miners to"))
        self.stratuminfo1_lbl = wx.StaticText(self, -1, _("host: localhost port: 8332"))
        self.balance_lbl = wx.StaticText(self, -1, _("Balance:"))
        self.balance_amt = wx.StaticText(self, -1, "0")
        self.balance_refresh = wx.Button(self, -1, STR_REFRESH_BALANCE)
        self.balance_refresh_timer = wx.Timer()
        self.withdraw = wx.Button(self, -1, _("Withdraw"))
        self.balance_cooldown_seconds = 0
        self.balance_auth_token = ""

        self.labels = [self.minercgminer_lbl, self.minerreaper_lbl, 
                      self.proxy_lbl, self.server_lbl, 
                      self.website_lbl, self.host_lbl, 
                      self.port_lbl, self.user_lbl, 
                      self.pass_lbl, self.device_lbl, 
                      self.thrcon_lbl, self.vectors_lbl, 
                      self.intensity_lbl, self.gputhreads_lbl, 
                      self.worksize_lbl, self.stratum_lbl, 
                      self.stratuminfo0_lbl, self.stratuminfo1_lbl, 
                      self.flags_lbl, self.balance_lbl, 
                      self.interactive_lbl, self.texcache_lbl, 
                      self.singlemem_lbl, self.warpflags_lbl,
                      self.minercudaminer_lbl]
        self.txts = [self.txt_host, self.txt_port,
                     self.txt_username, self.txt_pass,
                     self.txt_thrcon, self.txt_worksize, 
                     self.txt_vectors, self.txt_intensity, 
                     self.txt_gputhreads, self.txt_stratum, 
                     self.txt_flags, self.txt_interactive, 
                     self.txt_texcache, self.txt_singlemem,
                     self.txt_warpflags]
        self.all_widgets = [self.server, 
                            self.website,
                            self.device_listbox,
                            self.balance_amt,
                            self.balance_refresh,
                            self.withdraw] + self.labels + self.txts
        self.hidden_widgets = [self.extra_info,
                               self.txt_external,
                               self.external_lbl]

        self.start = wx.Button(self, -1, STR_START_MINING)

        self.device_listbox.SetSelection(0)
        self.server.SetStringSelection(self.defaults.get('default_server'))

        self.set_data(data)

        for txt in self.txts:
            txt.Bind(wx.EVT_KEY_UP, self.check_if_modified)
        self.device_listbox.Bind(wx.EVT_COMBOBOX, self.check_if_modified)

        self.start.Bind(wx.EVT_BUTTON, self.toggle_mining)
        self.server.Bind(wx.EVT_COMBOBOX, self.on_select_server)
        self.gpusettings.Bind(wx.EVT_COMBOBOX, self.on_select_gpusettings)
        self.balance_refresh_timer.Bind(wx.EVT_TIMER, self.on_balance_cooldown_tick)
        self.balance_refresh.Bind(wx.EVT_BUTTON, self.on_balance_refresh)
        self.withdraw.Bind(wx.EVT_BUTTON, self.on_withdraw)
        #for chk in self.affinity_chks:
        #    chk.Bind(wx.EVT_CHECKBOX, self.on_affinity_check)
        self.Bind(EVT_UPDATE_HASHRATE, lambda event: self.update_khash(event.rate))
        self.Bind(EVT_UPDATE_ACCEPTED, lambda event: self.update_shares(event.accepted))
        self.Bind(EVT_REAPER_ATTRIBUTE_UPDATE, lambda event: self.update_attributes_reaper(event.clstring))
        self.Bind(EVT_UPDATE_REAPER_ACCEPTED, lambda event: self.update_shares_reaper(event.quantity, event.accepted))
        self.Bind(EVT_UPDATE_STATUS, lambda event: self.update_status(event.text))
        self.Bind(EVT_UPDATE_SOLOCHECK, lambda event: self.update_solo())
        self.update_statusbar()
        self.clear_summary_widgets()

    @property
    def last_update_time(self):
        """Return the local time of the last accepted share."""
        if self.accepted_times:
            return time.localtime(self.accepted_times[-1])
        return None

    @property
    def server_config(self):
        hostname = self.txt_host.GetValue()
        return self.get_server_by_field(hostname, 'host')
        
    @property
    def gpusettings_config(self):
        profilename = self.gpusettings_data.GetValue()
        return self.get_gpusettings_by_field(profilename, 'name')

    @property
    def is_solo(self):
        """Return True if this miner is configured for solo mining."""
        return self.server.GetStringSelection() == "solo"

    @property
    def is_modified(self):
        """Return True if this miner has unsaved changes pending."""
        return self.last_data != self.get_data()

    @property
    def external_path(self):
        """Return the path to an external miner, or "" if none is present."""
        return self.txt_external.GetValue()

    @property
    def is_external_miner(self):
        """Return True if this miner has an external path configured."""
        return self.txt_external.GetValue() != ""

    @property
    def host_with_http_prefix(self):
        """Return the host address, with http:// prepended if needed."""
        host = self.txt_host.GetValue()
        if not host.startswith("http://"):
            host = "http://" + host
        return host

    @property
    def host_without_http_prefix(self):
        """Return the host address, with http:// stripped off if needed."""
        host = self.txt_host.GetValue()
        if host.startswith("http://"):
            return host[len('http://'):]
        return host

    @property
    def device_index(self):
        """Return the index of the currently selected OpenCL device."""
        s = self.device_listbox.GetStringSelection()
        match = re.search(r'\[(\d+)-(\d+)\]', s)
        try: return int(match.group(2))
        except: return 0

    @property
    def platform_index(self):
        """Return the index of the currently selected OpenCL platform."""
        s = self.device_listbox.GetStringSelection()
        match = re.search(r'\[(\d+)-(\d+)\]', s)
        try: return int(match.group(1))
        except: return 0

    @property
    def is_device_visible(self):
        """Return True if we are using a backend with device selection."""
        NO_DEVICE_SELECTION = ['rpcminer', 'bitcoin-miner']
        return not any(d in self.external_path for d in NO_DEVICE_SELECTION)

    def on_affinity_check(self, event):
        """Set the affinity mask to the selected value."""
        self.affinity_mask = 0
        for i in range(self.num_processors):
            # is_checked = self.affinity_chks[i].GetValue()
            self.affinity_mask += (is_checked << i)
        if self.is_mining:
            try:
                set_process_affinity(self.miner.pid, self.affinity_mask)
            except:
                pass # TODO: test on Linux

    def pause(self):
        """Pause the miner if we are mining, otherwise do nothing."""
        if self.is_mining:
            self.stop_mining()
            self.is_paused = True

    def resume(self):
        """Resume the miner if we are paused, otherwise do nothing."""
        if self.is_paused:
            self.start_mining()
            self.is_paused = False

    def get_data(self):
        """Return a dict of our profile data."""
        return dict(name=self.name,
                    hostname=self.txt_host.GetValue(),
                    port=self.txt_port.GetValue(),
                    username=self.txt_username.GetValue(),
                    password=self.txt_pass.GetValue(),
                    device=self.device_listbox.GetSelection(),
                    flags=self.txt_flags.GetValue(),
                    thrcon=self.txt_thrcon.GetValue(),
                    worksize=self.txt_worksize.GetValue(),
                    vectors=self.txt_vectors.GetValue(),
                    intensity=self.txt_intensity.GetValue(),
                    gputhreads=self.txt_gputhreads.GetValue(),
                    stratum=self.txt_stratum.GetValue(),
                    autostart=self.autostart,
                    affinity_mask=self.affinity_mask,
                    balance_auth_token=self.balance_auth_token,
                    interactive=self.txt_interactive.GetValue(),
                    texcache=self.txt_texcache.GetValue(),
                    singlemem=self.txt_singlemem.GetValue(),
                    warpflags=self.txt_warpflags.GetValue(),
                    external_path=self.external_path)

    def set_data(self, data):
        """Set our profile data to the information in data. See get_data()."""
        self.last_data = data
        default_server_config = self.get_server_by_field(
                                    self.defaults['default_server'], 'name')
        self.name = (data.get('name') or _('Default'))

        # Backwards compatibility: hostname key used to be called server.
        # We only save out hostname now but accept server from old INI files.
        hostname = (data.get('hostname') or _('')) # Hack by tacotime, don't give it any host, the user can enter it
        external_path_ref = (data.get('external_path') or _('CGMINER')) # Default miner is cgminer
        
        self.txt_host.SetValue(hostname)
        self.txt_external.SetValue(external_path_ref)
        self.txt_thrcon.SetValue(data.get('thrcon') or _(''))
        self.txt_worksize.SetValue(data.get('worksize') or _(''))
        self.txt_vectors.SetValue(data.get('vectors') or _(''))
        self.txt_intensity.SetValue(data.get('intensity') or _(''))
        self.txt_gputhreads.SetValue(data.get('gputhreads') or _(''))
        self.txt_stratum.SetValue(data.get('stratum') or _('Yes'))
        self.txt_interactive.SetValue(data.get('interactive') or _('Yes'))
        self.txt_texcache.SetValue(data.get('texcache') or _('Disabled'))
        self.txt_singlemem.SetValue(data.get('singlemem') or _('Yes'))
        self.txt_warpflags.SetValue(data.get('warpflags') or _('auto'))
        
        self.server.SetStringSelection(self.server_config.get('name', "Other"))

        self.txt_username.SetValue(
            data.get('username') or
            self.defaults.get('default_username', ''))

        self.txt_pass.SetValue(
            data.get('password') or
            self.defaults.get('default_password', ''))

        self.txt_port.SetValue(str(
            data.get('port') or
            self.server_config.get('port', 3333)))

        self.txt_flags.SetValue(data.get('flags', ''))
        self.autostart = data.get('autostart', False)
        self.affinity_mask = data.get('affinity_mask', 1)
        for i in range(self.num_processors):
            # self.affinity_chks[i].SetValue((self.affinity_mask >> i) & 1)
            pass

        # Handle case where they removed devices since last run.
        device_index = data.get('device', None)
        if device_index is not None and device_index < self.device_listbox.GetCount():
            self.device_listbox.SetSelection(device_index)

        # self.change_gpusettings(self.gpusettings_config)
        self.change_server(self.server_config)

        self.balance_auth_token = data.get('balance_auth_token', '')

    def clear_summary_widgets(self):
        """Release all our summary widgets."""
        self.summary_name = None
        self.summary_status = None
        self.summary_shares_accepted = None
        self.summary_shares_stale = None
        self.summary_start = None
        self.summary_autostart = None

    def get_start_stop_state(self):
        """Return appropriate text for the start/stop button."""
        return _("Stop") if self.is_mining else _("Start")

    def get_start_label(self):
        return STR_STOP_MINING if self.is_mining else STR_START_MINING

    def update_summary(self):
        """Update our summary fields if possible."""
        if not self.summary_panel:
            return

        self.summary_name.SetLabel(self.name)
        if self.is_paused:
            text = STR_PAUSED
        elif not self.is_mining:
            text = STR_STOPPED
        elif self.is_possible_error:
            text = _("Connection problems")
        elif (self.last_rate == -0.0000001):
            text = _("Proxy connected")
        else:
            text = format_khash(self.last_rate)
            
        self.summary_status.SetLabel(text)
        
        # Original
        # self.summary_shares_accepted.SetLabel("%d (%d)" % 
        #     (self.accepted_shares, len(self.accepted_times)))
        # New - Don't care about accepted_times since reaper doesn't have them - TacoTime
        self.summary_shares_accepted.SetLabel("%d" % 
            (self.accepted_shares))

        # Original
        # if self.is_solo:
        #    self.summary_shares_invalid.SetLabel("-")
        # else:
        #     self.summary_shares_invalid.SetLabel("%d (%d)" % 
        #         (self.invalid_shares, len(self.invalid_times)))
        # New - Don't care about invalid_times since reaper doesn't have them - TacoTime
        if self.is_solo:
            self.summary_shares_invalid.SetLabel("-")
        else:
            self.summary_shares_invalid.SetLabel("%d" % 
                (self.invalid_shares))

        self.summary_start.SetLabel(self.get_start_stop_state())
        self.summary_autostart.SetValue(self.autostart)
        self.summary_panel.grid.Layout()

    def get_summary_widgets(self, summary_panel):
        """Return a list of summary widgets suitable for sizer.AddMany."""
        self.summary_panel = summary_panel
        self.summary_name = wx.StaticText(summary_panel, -1, self.name)
        self.summary_name.Bind(wx.EVT_LEFT_UP, self.show_this_panel)

        self.summary_status = wx.StaticText(summary_panel, -1, STR_STOPPED)
        self.summary_shares_accepted = wx.StaticText(summary_panel, -1, "0")
        self.summary_shares_invalid = wx.StaticText(summary_panel, -1, "0")
        self.summary_start = wx.Button(summary_panel, -1, self.get_start_stop_state(), style=wx.BU_EXACTFIT)
        self.summary_start.Bind(wx.EVT_BUTTON, self.toggle_mining)
        self.summary_autostart = wx.CheckBox(summary_panel, -1)
        self.summary_autostart.Bind(wx.EVT_CHECKBOX, self.toggle_autostart)
        self.summary_autostart.SetValue(self.autostart)
        return [
            (self.summary_name, 0, wx.ALIGN_CENTER_HORIZONTAL),
            (self.summary_status, 0, wx.ALIGN_CENTER_HORIZONTAL, 0),
            (self.summary_shares_accepted, 0, wx.ALIGN_CENTER_HORIZONTAL, 0),
            (self.summary_shares_invalid, 0, wx.ALIGN_CENTER_HORIZONTAL, 0),
            (self.summary_start, 0, wx.ALIGN_CENTER, 0),
            (self.summary_autostart, 0, wx.ALIGN_CENTER, 0)
        ]

    def show_this_panel(self, event):
        """Set focus to this panel."""
        self.parent.SetSelection(self.parent.GetPageIndex(self))

    def toggle_autostart(self, event):
        self.autostart = event.IsChecked()

    def toggle_mining(self, event):
        """Stop or start the miner."""
        if self.is_mining:
            self.stop_mining()
        else:
            self.start_mining()
        self.update_summary()

    #############################
    # Begin backend specific code
    def configure_subprocess_poclbm(self):
        """Set up the command line for poclbm."""
        folder = get_module_path()
        if USE_MOCK:
            executable = "python mockBitcoinMiner.py"
        else:
            if hasattr(sys, 'frozen'):
                executable = "poclbm.exe"
            else:
                executable = "python poclbm.py"
        cmd = "%s %s:%s@%s:%s --device=%d --platform=%d --verbose -r1 %s" % (
                executable,
                self.txt_username.GetValue(),
                self.txt_pass.GetValue(),
                self.txt_host.GetValue(),
                self.txt_port.GetValue(),
                self.device_index,
                self.platform_index,
                self.txt_flags.GetValue()
        )
        return cmd, folder

    def configure_subprocess_rpcminer(self):
        """Set up the command line for rpcminer.

        The hostname must start with http:// for these miners.
        """
        cmd = "%s -user=%s -password=%s -url=%s:%s %s" % (
            self.external_path,
            self.txt_username.GetValue(),
            self.txt_pass.GetValue(),
            self.host_with_http_prefix,
            self.txt_port.GetValue(),
            self.txt_flags.GetValue()
        )
        return cmd, os.path.dirname(self.external_path)

    def configure_subprocess_ufasoft(self):
        """Set up the command line for ufasoft's SSE2 miner.

        The hostname must start with http:// for these miners.
        """
        cmd = "%s -u %s -p %s -o %s:%s %s" % (
            self.external_path,
            self.txt_username.GetValue(),
            self.txt_pass.GetValue(),
            self.host_with_http_prefix,
            self.txt_port.GetValue(),
            self.txt_flags.GetValue())
        return cmd, os.path.dirname(self.external_path)

    def configure_subprocess_phoenix(self):
        """Set up the command line for phoenix miner."""
        path = self.external_path
        if path.endswith('.py'):
            path = "python " + path

        cmd = "%s -u http://%s:%s@%s:%s PLATFORM=%d DEVICE=%d %s" % (
            path,
            self.txt_username.GetValue(),
            self.txt_pass.GetValue(),
            self.host_without_http_prefix,
            self.txt_port.GetValue(),
            self.platform_index,
            self.device_index,
            self.txt_flags.GetValue())
        return cmd, os.path.dirname(self.external_path)

    def configure_subprocess_cgminer(self):
        """Set up the command line for cgminer."""

        # Set the path for cgminer, should be ./cgminer/cgminer.exe
        # Not set up for unix, modify this to /cgminer/cgminer for unix
        os.chdir(STARTUP_PATH)
        path = '\"' + STARTUP_PATH + "\\cgminer\\cgminer.exe" + '\"'
        cgdir = STARTUP_PATH + "\\cgminer\\"
        
        #if path.endswith('.py'):
        #    path = "python " + path
        
        if self.txt_stratum.GetValue() == "Yes":
            http_header = "stratum+tcp://"
        else:
            http_header = "http://"

        # Command line arguments for cgminer here:
        # -u <username>
        # -p <password>
        # -o <http://server.ip:port>
        # --gpu-platform <like it sounds>
        # -w <worksize>
        # -v <vectors>
        # -d <device appear in pyopencl>
        # -l <log message period in second>
        # -T <disable curses interface and output to console (stdout)>
        # -g <GPU threads>
        cmd = "%s --scrypt -u %s -p %s -o %s%s:%s --gpu-platform %s -d %s -w %s -v %s -I %s -g %s -l 1 -T %s --thread-concurrency %s" % (
            path,
            self.txt_username.GetValue(),
            self.txt_pass.GetValue(),
            http_header,
            self.host_without_http_prefix,
            self.txt_port.GetValue(),
            self.platform_index,
            self.device_index,
            self.txt_worksize.GetValue(),
            self.txt_vectors.GetValue(),
            self.txt_intensity.GetValue(),
            self.txt_gputhreads.GetValue(),
            self.txt_flags.GetValue(),
            self.txt_thrcon.GetValue())
        
        # Full console command for batch file creation given below; don't add -T in this instance so end user gets full output
        full_console_cmd = "%s --scrypt -u %s -p %s -o %s%s:%s --gpu-platform %s -d %s -w %s -v %s -I %s -g %s -l 1 %s --thread-concurrency %s" % (
            path,
            self.txt_username.GetValue(),
            self.txt_pass.GetValue(),
            http_header,
            self.host_without_http_prefix,
            self.txt_port.GetValue(),
            self.platform_index,
            self.device_index,
            self.txt_worksize.GetValue(),
            self.txt_vectors.GetValue(),
            self.txt_intensity.GetValue(),
            self.txt_gputhreads.GetValue(),
            self.txt_flags.GetValue(),
            self.txt_thrcon.GetValue())
            
        f = open(cgdir + "mine-" +  self.name + ".bat", 'w')
        f.write(full_console_cmd)
        f.close()
        
        return cmd, os.path.dirname(path)

    def write_reaper_configs(self, reaperdir):
        # reaper.conf
        f = open(reaperdir + "\\reaper.conf", 'w')
        f.write("kernel reaper.cl\n")
        f.write("save_binaries yes\n")
        f.write("enable_graceful_shutdown no\n")
        f.write("long_polling yes\n")
        f.write("platform " + str(self.platform_index) + "\n")
        f.write("device " + str(self.device_index) + "\n\n")
        f.write("mine litecoin\n")
        f.close()
        
        # litecoin.conf
        f = open(reaperdir + "\\litecoin.conf", 'w')
        f.write("host " + self.host_without_http_prefix + "\n")
        f.write("port " + self.txt_port.GetValue() + "\n")
        f.write("user " + self.txt_username.GetValue() + "\n")
        f.write("pass " + self.txt_pass.GetValue() + "\n\n")
        f.write("protocol litecoin\n\n")
        f.write("gpu_thread_concurrency " + self.txt_thrcon.GetValue() + "\n")
        f.write("worksize " + self.txt_worksize.GetValue() + "\n")
        f.write("vectors " + self.txt_vectors.GetValue() + "\n")
        f.write("aggression " + self.txt_intensity.GetValue() + "\n")
        f.write("threads_per_gpu " + self.txt_gputhreads.GetValue() + "\n")
        f.write("sharethreads 32\n")
        f.write("lookup_gap 2\n")
        f.close()
        
    def configure_subprocess_reaper(self):
        """Set up the command line for reaper."""
        os.chdir(STARTUP_PATH)
        
        if os.path.exists(STARTUP_PATH + "\\reaper"):
            if os.path.exists(STARTUP_PATH + "\\reaper-" + self.name):
                logger.info("Reaper folder for miner already exists, writing config and commencing with mining.")
                self.write_reaper_configs(STARTUP_PATH + "\\reaper-" + self.name)
            else:
                logger.info("Reaper folder for miner missing, adding folder, files, and config.")
                os.makedirs(STARTUP_PATH + "\\reaper-" + self.name)
                distutils.dir_util.copy_tree(STARTUP_PATH + "\\reaper", os.getcwd() + "\\reaper-" + self.name)
                self.write_reaper_configs(STARTUP_PATH + "\\reaper-" + self.name)
        else:
            logger.info("Reaper folder with binaries is missing; can not mine!  Add reaper to ./reaper/ folder please.")

        path = STARTUP_PATH + "\\reaper-" + self.name

        # Have to change working directory, windows pain in the ass for reaper - TacoTime
        os.chdir(path)
        cmd =  '\"' + path + "\\reaper.exe" +  '\"' # Change this for unix!!!  - TacoTime
        return cmd, os.path.dirname(path)      
        
    def configure_subprocess_cudaminer(self):
        os.chdir(STARTUP_PATH)
        path =  '\"' + STARTUP_PATH + "\\cudaminer\\cudaminer.exe" + '\"' # Change this for unix!!!  - TacoTime
        
        os.chdir(STARTUP_PATH)
        cudaminerpath = STARTUP_PATH + "\\cudaminer\\"
        
        flag_interactive = 0 # Flag for cudaminer interactive setting
        if (self.txt_interactive.GetValue() == "Yes"):
            flag_interactive = 1
        else:
            flag_interactive = 0

        flag_texcache = 1
        if (self.txt_texcache.GetValue() == "Disabled"):
            flag_texcache = 0
        elif (self.txt_texcache.GetValue() == "1D"):
            flag_texcache = 1
        else:
            flag_texcache = 2
            
        flag_singlemem = 1
        if (self.txt_texcache.GetValue() == "Yes"):
            flag_singlemem = 0
        else:
            flag_singlemem = 1
            
        # Command line arguments for cudaminer here:
        # -o host and port prefixed with http://
        # -O username:password
        # -d device number (CUDA platform)
        # -i interactive (bool)
        # -l kernel/warp configuration (string len 4 or 5?)
        # -C Texture cache (0=disabled, 1=1D, 2=2D)
        # -m Single memory block (bool)
        cmd = "%s -o http://%s:%s/ -O %s:%s -d %s -i %s -l %s -C %s -m %s" % (
            path,
            self.host_without_http_prefix,
            self.txt_port.GetValue(),
            self.txt_username.GetValue(),
            self.txt_pass.GetValue(),
            self.device_index,
            flag_interactive,
            self.txt_warpflags.GetValue(),
            flag_texcache,
            flag_singlemem)
            
        # Create a batch file in case the user wants to try it out in console, too 
        f = open(cudaminerpath + "mine-" +  self.name + ".bat", 'w')
        f.write(cmd)
        f.close()
        
        return cmd, os.path.dirname(path)   
        
    def configure_subprocess_stratumproxy(self):
        """Set up the command line for proxy miner."""
        os.chdir(STARTUP_PATH)
        path = STARTUP_PATH + "\\stratumproxy\\mining_proxy.exe"
        if path.endswith('.py'):
            path = "python " + path

        # Command line arguments for cgminer here:
        # -u <username>
        # -p <password>
        # -o <http://server.ip:port>
        # -d <device appear in pyopencl>
        # -l <log message period in second>
        # -T <disable curses interface and output to console (stdout)>
        cmd = "%s -pa scrypt -o %s -p %s %s" % (
            path,
            self.host_without_http_prefix,
            self.txt_port.GetValue(),
            self.txt_flags.GetValue())
        return cmd, os.path.dirname(path)

    # End backend specific code
    ###########################

    def start_mining(self):
        """Launch a miner subprocess and attach a MinerListenerThread."""
        self.is_paused = False

        # Avoid showing a console window when frozen
        try: import win32process
        except ImportError: flags = 0
        else: flags = win32process.CREATE_NO_WINDOW

        # Determine what command line arguments to use

        listener_cls = MinerListenerThread
        
        if not self.is_external_miner:
            conf_func = self.configure_subprocess_poclbm
        elif "rpcminer" in self.external_path:
            conf_func = self.configure_subprocess_rpcminer
        elif "bitcoin-miner" in self.external_path:
            conf_func = self.configure_subprocess_ufasoft
        elif "phoenix" in self.external_path:
            conf_func = self.configure_subprocess_phoenix
            listener_cls = PhoenixListenerThread
        elif "CGMINER" in self.external_path:
            conf_func = self.configure_subprocess_cgminer
            listener_cls = CgListenerThread
        elif "REAPER" in self.external_path:
            conf_func = self.configure_subprocess_reaper
            listener_cls = ReaperListenerThread
        elif "CUDAMINER" in self.external_path:
            conf_func = self.configure_subprocess_cudaminer
            listener_cls = CudaminerListenerThread
        elif "PROXY" in self.external_path:
            conf_func = self.configure_subprocess_stratumproxy
            listener_cls = ProxyListenerThread
        else:
            raise ValueError # TODO: handle unrecognized miner
        cmd, cwd = conf_func()

        # for ufasoft:
        #  redirect stderr to stdout
        #  use universal_newlines to catch the \r output on Mhash/s lines
        try:
            logger.debug(_('Running command: ') + cmd)
            # for cgminer: 
            if conf_func == self.configure_subprocess_cgminer:
                cgminer_env = os.environ # Create an environment to set below environmental variable in
                cgminer_env['GPU_MAX_ALLOC_PERCENT'] = "100" # Set this environmental variable so we can use high thread concurrencies in cgminer
                self.miner = subprocess.Popen(cmd,
                                              env=cgminer_env,
                                              stdout=subprocess.PIPE,
                                              stderr=None,
                                              universal_newlines=True,
                                              creationflags=0x08000000,
                                              shell=(sys.platform != 'win32'))
            else:
                self.miner = subprocess.Popen(cmd,
                                              stdout=subprocess.PIPE,
                                              stderr=subprocess.STDOUT,
                                              universal_newlines=True,
                                              creationflags=0x08000000,
                                              shell=(sys.platform != 'win32'))
            
        except OSError:
            raise #TODO: the folder or exe could not exist
        self.miner_listener = listener_cls(self, self.miner)
        self.miner_listener.daemon = True
        self.miner_listener.start()
        self.is_mining = True
        self.set_status(STR_STARTING, 1)
        self.start.SetLabel(self.get_start_label())

        try:
            set_process_affinity(self.miner.pid, self.affinity_mask)
        except:
            pass # TODO: test on Linux

    def on_close(self):
        """Prepare to close gracefully."""
        self.stop_mining()
        self.balance_refresh_timer.Stop()

    def stop_mining(self):
        """Terminate the poclbm process if able and its associated listener."""
        if self.miner is not None:
            if self.miner.returncode is None:
                # It didn't return yet so it's still running.
                try:
                    self.miner.terminate()
                except OSError:
                    pass # TODO: Guess it wasn't still running?
            self.miner = None
        if self.miner_listener is not None:
            self.miner_listener.shutdown_event.set()
            self.miner_listener = None
        self.is_mining = False
        self.is_paused = False
        self.set_status(STR_STOPPED, 1)
        self.start.SetLabel(self.get_start_label())

    def update_khash(self, rate):
        """Update our rate according to a report from the listener thread.

        If we are receiving rate messages then it means poclbm is no longer
        reporting errors.
        """
        self.last_rate = rate
        self.set_status(format_khash(rate), 1)
        
        if self.is_possible_error:
            self.update_statusbar()
            self.is_possible_error = False
            
    def update_shares_reaper(self, quantity, accepted):
        if self.is_solo:
            self.solo_blocks_found = quantity
        elif accepted:
            self.accepted_shares = quantity
        else:
            self.invalid_shares = quantity
        self.update_last_time(accepted) # BUG: This doesn't work right, but let's ignore it for now - TacoTime
        self.update_statusbar()
        
    def update_attributes_reaper(self, clstring):
        sharesA = int(non_decimal.sub('', re.search(r"shares: *\d+\|", clstring).group(0)))
        sharesR = int(non_decimal.sub('', re.search(r"\|\d+,", clstring).group(0)))
        hashrate = float(non_decimal.sub('', re.search(r"\~.*kH", clstring).group(0)))
        
        if self.is_solo:
            self.solo_blocks_found = sharesA
        else:
            self.accepted_shares = sharesA
        
        self.invalid_shares = sharesR
        self.last_rate = hashrate
        self.set_status(format_khash(hashrate), 1)
        if self.is_possible_error:
            self.update_statusbar()
            self.is_possible_error = False
        self.update_statusbar()

    def update_statusbar(self):
        """Show the shares or equivalent on the statusbar."""
        if self.is_solo:
            text = _("Difficulty 1 hashes: %(nhashes)d %(update_time)s") % \
                dict(nhashes=self.accepted_shares,
                     update_time=self.format_last_update_time())
            if self.solo_blocks_found > 0:
                block_text = _("Blocks: %d, ") % self.solo_blocks_found
                text = block_text + text
        else:
            text = _("Shares: %d accepted") % self.accepted_shares
            if self.invalid_shares > 0:
                text += _(", %d stale/invalid") % self.invalid_shares
            text += " %s" % self.format_last_update_time()
        self.set_status(text, 0)

    def update_last_time(self, accepted):
        """Set the last update time to now (in local time)."""

        now = time.time()
        if accepted:
            self.accepted_times.append(now)
            while now - self.accepted_times[0] > SAMPLE_TIME_SECS:
                self.accepted_times.popleft()
        else:
            self.invalid_times.append(now)
            while now - self.invalid_times[0] > SAMPLE_TIME_SECS:
                self.invalid_times.popleft()

    def format_last_update_time(self):
        """Format last update time for display."""
        time_fmt = '%I:%M:%S%p'
        if self.last_update_time is None:
            return ""
        return _("- last at %s") % time.strftime(time_fmt, self.last_update_time)

    def update_shares(self, accepted):
        """Update our shares with a report from the listener thread."""
        if self.is_solo and accepted:
            self.solo_blocks_found += 1
        elif accepted:
            self.accepted_shares += 1
        else:
            self.invalid_shares += 1
        self.update_last_time(accepted)
        self.update_statusbar()

    def update_status(self, msg):
        """Update our status with a report from the listener thread.

        If we receive a message from poclbm we don't know how to interpret,
        it's probably some kind of error state - in this case the best
        thing to do is just show it to the user on the status bar.
        """
        self.set_status(msg)
        if self.last_rate == -0.0000001:
            self.is_possible_error = False
        else:
            self.is_possible_error = True

    def set_status(self, msg, index=0):
        """Set the current statusbar text, but only if we have focus."""
        if self.parent.GetSelection() == self.parent.GetPageIndex(self):
            self.statusbar.SetStatusText(msg, index)

    def on_focus(self):
        """When we receive focus, update our status.

        This ensures that when switching tabs, the statusbar always
        shows the current tab's status.
        """
        self.update_statusbar()
        if self.is_mining:
            self.update_khash(self.last_rate)
        else:
            self.set_status(STR_STOPPED, 1)

    def get_taskbar_text(self):
        """Return text for the hover state of the taskbar."""
        rate = format_khash(self.last_rate) if self.is_mining else STR_STOPPED
        return "%s: %s" % (self.name, rate)

    def update_solo(self):
        """Update our easy hashes with a report from the listener thread."""
        self.accepted_shares += 1
        self.update_last_time(True)
        self.update_statusbar()
        
    def on_select_gpusettings(self, event):
        """Update our info in response to a new server choice."""
        new_gpusettings_name = str(self.gpusettings.GetValue())
        new_gpusettings = self.get_gpusettings_by_field(new_gpusettings_name, 'name')
        self.change_gpusettings(new_gpusettings)

    def on_select_server(self, event):
        """Update our info in response to a new server choice."""
        print self.server.GetValue()
        new_server_name = self.server.GetValue()
        new_server = self.get_server_by_field(new_server_name, 'name')
        self.change_server(new_server)

    def get_gpusettings_by_field(self, target_val, field):
        """Return the first server dict with the specified val, or {}."""
        for s in self.gpusettings_data:
            if s.get(field) == target_val:
                return s
        return {}        
        
    def get_server_by_field(self, target_val, field):
        """Return the first server dict with the specified val, or {}."""
        for s in self.servers:
            if s.get(field) == target_val:
                return s
        return {}

    def set_widgets_visible(self, widgets, show=False):
        """Show or hide each widget in widgets according to the show flag."""
        for w in widgets:
            if show:
                w.Show()
            else:
                w.Hide()

    def set_tooltips(self):
        add_tooltip(self.server, _("Server to connect to. Different servers have different fees and features.\nCheck their websites for full information."))
        add_tooltip(self.website, _("Website of the currently selected server. Click to visit."))
        add_tooltip(self.device_listbox, _("Available OpenCL devices on your system."))
        add_tooltip(self.txt_host, _("Host address, without http:// prefix."))
        add_tooltip(self.txt_port, _("Server port. This is usually 8332 for getwork or 3333 for stratum."))
        add_tooltip(self.txt_username, _("The miner's username.\nMay be different than your account username.\nExample: Kiv.GPU"))
        add_tooltip(self.txt_pass, _("The miner's password.\nMay be different than your account password."))
        add_tooltip(self.txt_flags, _("Extra flags to pass to the miner."))
        add_tooltip(self.txt_thrcon, _("Set the memory size for the scrypt kernel to use.\n1 unit = 64 KB"))
        add_tooltip(self.txt_worksize, _("Set the worksize value.\nDefault: 256"))
        add_tooltip(self.txt_vectors, _("Set the vectors value.\nDefault: 1"))
        add_tooltip(self.txt_gputhreads, _("Set the number of default threads to use.\nDefault: 1"))
        add_tooltip(self.txt_intensity, _("Set the intensity/aggression value.\nHigh intensity: 18-20\nLow intensity: 10-14"))
        add_tooltip(self.gpusettings, _("Default values for any given AMD video card.\nTry these first if you are new to scrypt mining."))
        add_tooltip(self.txt_interactive, _("Run in interactive mode so that the desktop remains usable while mining.\nMay slow hash rate."))
        add_tooltip(self.txt_texcache, _("Enable use of 1D or 2D texture cache for mining."))
        add_tooltip(self.txt_singlemem, _("Use multiple blocks of memory or a single block of memory for mining."))
        add_tooltip(self.txt_warpflags, _("String in S##x# or ##x# format that gives the warp configuration.\nExamples: S27x3 or 28x4.\nUse auto for automatic warp configuration tuning."))
        #for chk in self.affinity_chks:
        #    add_tooltip(chk, _("CPU cores used for mining.\nUnchecking some cores can reduce high CPU usage in some systems."))

    def reset_statistics(self):
        """Reset our share statistics to zero."""
        self.solo_blocks_found = 0
        self.accepted_shares = 0
        self.accepted_times.clear()
        self.invalid_shares = 0
        self.invalid_times.clear()
        self.update_statusbar()
        
    def change_gpusettings(self, new_gpusettings):
        self.reset_statistics()
        if 'thread_concurrency' in new_gpusettings:
            self.txt_thrcon.SetValue(str(new_gpusettings['thread_concurrency']))
        if 'worksize' in new_gpusettings:
            self.txt_worksize.SetValue(str(new_gpusettings['worksize']))
        if 'vectors' in new_gpusettings:
            self.txt_vectors.SetValue(str(new_gpusettings['vectors']))
        if 'gputhreads' in new_gpusettings:
            self.txt_gputhreads.SetValue(str(new_gpusettings['gputhreads']))
        if 'intensity' in new_gpusettings:
            self.txt_intensity.SetValue(str(new_gpusettings['intensity']))

    def change_server(self, new_server):
        """Change the server to new_server, updating fields as needed."""
        self.reset_statistics()

        # Set defaults before we do server specific code
        self.set_tooltips()
        self.set_widgets_visible(self.all_widgets, True)
        self.withdraw.Disable()

        url = new_server.get('url', 'n/a')
        self.website.SetLabel(url)
        self.website.SetURL(url)

        # Invalidate any previous auth token since it won't be valid for the
        # new server.
        self.balance_auth_token = ""

        if 'host' in new_server:
            self.txt_host.SetValue(new_server['host'])
        if 'port' in new_server:
            self.txt_port.SetValue(str(new_server['port']))


        # Call server specific code.
        host = new_server.get('host', "").lower()
        if host == "api2.bitcoin.cz" or host == "mtred.com": self.layout_slush()
        if "eligius.st" in host: self.layout_eligius()
        elif host == "bitpenny.dyndns.biz": self.layout_bitpenny()
        elif host == "pit.deepbit.net": self.layout_deepbit()
        elif host == "btcmine.com": self.layout_btcmine()
        elif host == "rr.btcmp.com": self.layout_btcmp()
        elif "btcguild.com" in host: self.layout_btcguild()
        elif host == "bitcoin-server.de": self.layout_bitcoinserver
        elif host == "pit.x8s.de": self.layout_x8s()
        elif self.external_path == "CUDAMINER": self.layout_cudaminer()
        else: self.layout_default()

        self.Layout()

        self.update_tab_name()

    def on_balance_cooldown_tick(self, event=None):
        """Each second, decrement the cooldown for refreshing balance."""
        self.balance_cooldown_seconds -= 1
        self.balance_refresh.SetLabel("%d..." % self.balance_cooldown_seconds)
        if self.balance_cooldown_seconds <= 0:
            self.balance_refresh_timer.Stop()
            self.balance_refresh.Enable()
            self.balance_refresh.SetLabel(STR_REFRESH_BALANCE)

    def require_auth_token(self):
        """Prompt the user for an auth token if they don't have one already.

        Set the result to self.balance_auth_token and return None.
        """
        if self.balance_auth_token:
            return
        url = self.server_config.get('balance_token_url')
        dialog = BalanceAuthRequest(self, url)
        dialog.txt_token.SetFocus()
        result = dialog.ShowModal()
        dialog.Destroy()
        if result == wx.ID_CANCEL:
            return
        self.balance_auth_token = dialog.get_value() # TODO: validate token?

    def is_auth_token_rejected(self, response):
        """If the server rejected our token, reset auth_token and return True.

        Otherwise, return False.
        """
        if response.status in [401, 403]: # 401 Unauthorized or 403 Forbidden
            # Token rejected by the server - reset their token so they'll be
            # prompted again
            self.balance_auth_token = ""
            return True
        return False

    def request_balance_get(self, balance_auth_token, use_https=False):
        """Request our balance from the server via HTTP GET and auth token.

        This method should be run in its own thread.
        """
        response, data = http_request(
            self.server_config['balance_host'],
            "GET",
            self.server_config["balance_url"] % balance_auth_token,
            use_https=use_https
        )
        if self.is_auth_token_rejected(response):
            data = _("Auth token rejected by server.")
        elif not data:
            data = STR_CONNECTION_ERROR
        else:
            try:
                info = json.loads(data)
                confirmed = (info.get('confirmed_reward') or
                             info.get('confirmed') or
                             info.get('balance') or
                             info.get('user', {}).get('confirmed_rewards') or
                             0)
                unconfirmed = (info.get('unconfirmed_reward') or
                               info.get('unconfirmed') or
                               info.get('user', {}).get('unconfirmed_rewards') or
                               0)
                if self.server_config.get('host') == "pit.deepbit.net":
                    ipa = info.get('ipa', False)
                    self.withdraw.Enable(ipa)

                if self.server_config.get('host') == "rr.btcmp.com":
                    ipa = info.get('can_payout', False)
                    self.withdraw.Enable(ipa)

                data = _("%s confirmed") % format_balance(confirmed)
                if unconfirmed > 0:
                    data += _(", %s unconfirmed") % format_balance(unconfirmed)
            except: # TODO: what exception here?
                data = _("Bad response from server.")

        wx.CallAfter(self.balance_amt.SetLabel, data)

    def on_withdraw(self, event):
        self.withdraw.Disable()
        host = self.server_config.get('host')
        if host == 'bitpenny.dyndns.biz':
            self.withdraw_bitpenny()
        elif host == 'pit.deepbit.net':
            self.withdraw_deepbit()
        elif host == 'rr.btcmp.com':
            self.withdraw_btcmp()

    def requires_auth_token(self, host):
        """Return True if the specified host requires an auth token for balance update."""
        HOSTS_REQUIRING_AUTH_TOKEN = ["api2.bitcoin.cz",
                                      "btcmine.com",
                                      "pit.deepbit.net",
                                      "pit.x8s.de",
                                      "mtred.com",
                                      "rr.btcmp.com",
                                      "bitcoin-server.de"]
        if host in HOSTS_REQUIRING_AUTH_TOKEN: return True        
        if "btcguild" in host: return True      
        return False
    
    def requires_https(self, host):
        """Return True if the specified host requires HTTPs for balance update."""
        HOSTS = ["mtred.com", "api2.bitcoin.cz"]
        if host in HOSTS: return True
        if "btcguild" in host: return True
        return False
    
    def on_balance_refresh(self, event=None):
        """Refresh the miner's balance from the server."""
        host = self.server_config.get("host")
        if self.requires_auth_token(host):
            self.require_auth_token()
            if not self.balance_auth_token: # They cancelled the dialog
                return
            try:
                self.balance_auth_token.decode('ascii')
            except UnicodeDecodeError:
                return # Invalid characters in auth token
            self.http_thread = threading.Thread(
                target=self.request_balance_get,
                args=(self.balance_auth_token,),
                kwargs=dict(use_https=self.requires_https(host)))
            self.http_thread.start()
        elif host == 'bitpenny.dyndns.biz':
            self.http_thread = threading.Thread(
            target=self.request_payout_bitpenny, args=(False,))
            self.http_thread.start()
        elif 'eligius.st' in host:
            self.http_thread = threading.Thread(
                target=self.request_balance_eligius
            )
            self.http_thread.start()

        self.balance_refresh.Disable()
        self.balance_cooldown_seconds = 10
        self.balance_refresh_timer.Start(1000)

    #################################
    # Begin server specific HTTP code

    def withdraw_btcmp(self):
        """Launch a thread to withdraw from deepbit."""
        self.require_auth_token()
        if not self.balance_auth_token: # User refused to provide token
            return
        self.http_thread = threading.Thread(
                target=self.request_payout_btcmp,
                args=(self.balance_auth_token,))
        self.http_thread.start()

    def withdraw_deepbit(self):
        """Launch a thread to withdraw from deepbit."""
        self.require_auth_token()
        if not self.balance_auth_token: # User refused to provide token
            return
        self.http_thread = threading.Thread(
                target=self.request_payout_deepbit,
                args=(self.balance_auth_token,))
        self.http_thread.start()

    def withdraw_bitpenny(self):
        self.http_thread = threading.Thread(
            target=self.request_payout_bitpenny, args=(True,))
        self.http_thread.start() # TODO: look at aliasing of this variable

    def request_payout_btcmp(self, balance_auth_token):
        """Request payout from btcmp's server via HTTP POST."""        
        response, data = http_request(
            self.server_config['balance_host'],
            "GET",
            self.server_config["payout_url"] % balance_auth_token,
            use_https=False
        )
        
        if self.is_auth_token_rejected(response):
            data = _("Auth token rejected by server.")
        elif not data:
            data = STR_CONNECTION_ERROR
        else:
            data = _("Withdraw OK")
        wx.CallAfter(self.on_balance_received, data)

    def request_payout_deepbit(self, balance_auth_token):
        """Request payout from deepbit's server via HTTP POST."""
        post_params = dict(id=1,
                           method="request_payout")
        response, data = http_request(
             self.server_config['balance_host'],
             "POST",
             self.server_config['balance_url'] % balance_auth_token,
             json.dumps(post_params),
             {"Content-type": "application/json; charset=utf-8",
              "User-Agent": USER_AGENT}
        )
        if self.is_auth_token_rejected(response):
            data = _("Auth token rejected by server.")
        elif not data:
            data = STR_CONNECTION_ERROR
        else:
            data = _("Withdraw OK")
        wx.CallAfter(self.on_balance_received, data)

    def request_payout_bitpenny(self, withdraw):
        """Request our balance from BitPenny via HTTP POST.

        If withdraw is True, also request a withdrawal.
        """
        post_params = dict(a=self.txt_username.GetValue(), w=int(withdraw))
        response, data = http_request(
             self.server_config['balance_host'],
             "POST",
             self.server_config['balance_url'],
             urllib.urlencode(post_params),
             {"Content-type": "application/x-www-form-urlencoded"}
        )
        if self.is_auth_token_rejected(response):
            data = _("Auth token rejected by server.")
        elif not data:
            data = STR_CONNECTION_ERROR
        elif withdraw:
            data = _("Withdraw OK")
        wx.CallAfter(self.on_balance_received, data)

    def request_balance_eligius(self):
        """Request our balance from Eligius
        """
        response, data = http_request(
             self.server_config['balance_host'],
             "POST",
             self.server_config['balance_url'] % (self.txt_username.GetValue(),),
        )
        if not data:
            data = STR_CONNECTION_ERROR
        try:
            data = json.loads(data)
            data = data['expected'] / 1e8
        except BaseException as e:
            data = str(e)
        wx.CallAfter(self.on_balance_received, data)

    def on_balance_received(self, balance):
        """Set the balance in the GUI."""
        try:
            amt = float(balance)
        except ValueError: # Response was some kind of error
            self.balance_amt.SetLabel(balance)
        else:
            if amt > 0.1:
                self.withdraw.Enable()
            amt_str = format_balance(amt)
            self.balance_amt.SetLabel(amt_str)
        self.Layout()

    # End server specific HTTP code
    ###############################

    def set_name(self, name):
        """Set the label on this miner's tab to name."""
        self.name = name
        if self.summary_name:
            self.summary_name.SetLabel(self.name)
        self.update_tab_name()

    def update_tab_name(self):
        """Update the tab name to reflect modified status."""
        name = self.name
        if self.is_modified:
            name += "*"
        page = self.parent.GetPageIndex(self)
        if page != -1:
            self.parent.SetPageText(page, name)

    def check_if_modified(self, event):
        """Update the title of the tab to have an asterisk if we are modified."""
        self.update_tab_name()
        event.Skip()

    def on_saved(self):
        """Update our last data after a save."""
        self.last_data = self.get_data()
        self.update_tab_name()

    def layout_init(self):
        """Create the sizers for this frame and set up the external text.

        Return the lowest row that is available.
        """
        self.frame_sizer = wx.BoxSizer(wx.VERTICAL)
        self.frame_sizer.Add((20, 10), 0, wx.EXPAND, 0) # Controls top window size
        self.inner_sizer = wx.GridBagSizer(10, 5) # Controls inner window height, width
        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
        row = 0
        # if self.is_external_miner:
            # self.inner_sizer.Add(self.external_lbl, (row, 0), flag=LBL_STYLE)
            # self.inner_sizer.Add(self.txt_external, (row, 1), span=(1, 3), flag=wx.EXPAND)
            # row += 1
        return row

    def layout_server_and_website(self, row):
        """Lay out the server and website widgets in the specified row."""
        self.inner_sizer.Add(self.server_lbl, (row, 0), flag=LBL_STYLE)
        self.inner_sizer.Add(self.server, (row, 1), flag=wx.EXPAND)
        self.inner_sizer.Add(self.website_lbl, (row, 2), flag=LBL_STYLE)
        self.inner_sizer.Add(self.website, (row, 3), flag=wx.ALIGN_CENTER_VERTICAL)
        
    def layout_minertype(self, row):
        """Display which miner is being used"""
        if self.external_path == "CGMINER":
            self.inner_sizer.Add(self.minercgminer_lbl, (row, 0), flag=LBL_STYLE)
        elif self.external_path == "REAPER":
            self.inner_sizer.Add(self.minerreaper_lbl, (row, 0), flag=LBL_STYLE)
        elif self.external_path == "CUDAMINER":
            self.inner_sizer.Add(self.minercudaminer_lbl, (row, 0), flag=LBL_STYLE)
        else:
            self.inner_sizer.Add(self.proxy_lbl, (row, 0), flag=LBL_STYLE)

    def layout_host_and_port(self, row):
        """Lay out the host and port widgets in the specified row."""
        self.inner_sizer.Add(self.host_lbl, (row, 0), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_host, (row, 1), flag=wx.EXPAND)
        self.inner_sizer.Add(self.port_lbl, (row, 2), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_port, (row, 3), flag=wx.ALIGN_CENTER_VERTICAL)

    def layout_user_and_pass(self, row):
        """
        Lay out the user and pass widgets in the specified row.
        Also used to help out users with stratum proxy.
        """
        if (self.external_path == "PROXY"):
            self.inner_sizer.Add(self.stratuminfo0_lbl, (row, 0), flag=wx.EXPAND)
        else:
            self.inner_sizer.Add(self.user_lbl, (row, 0), flag=LBL_STYLE)
        if  (self.external_path == "PROXY"):
            self.inner_sizer.Add(self.stratuminfo1_lbl, (row, 1), flag=wx.EXPAND)
        else:
            self.inner_sizer.Add(self.txt_username, (row, 1), flag=wx.EXPAND)
        self.inner_sizer.Add(self.pass_lbl, (row, 2), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_pass, (row, 3), flag=wx.EXPAND)    
    
    def layout_thrcon_worksize(self, row):
        """
        Like it sounds, thread concurrency and worksize boxes.
        """
        self.inner_sizer.Add(self.thrcon_lbl, (row, 0), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_thrcon, (row, 1), flag=wx.EXPAND)
        self.inner_sizer.Add(self.worksize_lbl, (row, 2), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_worksize, (row, 3), flag=wx.EXPAND)

    def layout_vectors_intensity(self, row):
        """
        Like it sounds, vector and intensity boxes.
        """
        self.inner_sizer.Add(self.vectors_lbl, (row, 0), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_vectors, (row, 1), flag=wx.EXPAND)
        self.inner_sizer.Add(self.intensity_lbl, (row, 2), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_intensity, (row, 3), flag=wx.EXPAND)
        
    def layout_gputhreads_gpusettings(self, row):
        """
        Like it sounds, no. gpu threads and gpu defaults
        """
        self.inner_sizer.Add(self.gputhreads_lbl, (row, 0), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_gputhreads, (row, 1), flag=wx.EXPAND)
        self.inner_sizer.Add(self.gpusettings_lbl, (row, 2), flag=LBL_STYLE)
        self.inner_sizer.Add(self.gpusettings, (row, 3), flag=wx.EXPAND)
        
    def layout_stratum(self, row):
        """
        Like it sounds, stratum boxes.
        """
        self.inner_sizer.Add(self.stratum_lbl, (row, 0), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_stratum, (row, 1), flag=wx.EXPAND)
    
    def layout_device_and_flags(self, row):
        """Lay out the device and flags widgets in the specified row.

        Hide the device dropdown if RPCMiner is present since it doesn't use it.
        """
        device_visible = self.is_device_visible
        self.set_widgets_visible([self.device_lbl, self.device_listbox], device_visible)
        if device_visible:
            self.inner_sizer.Add(self.device_lbl, (row, 0), flag=LBL_STYLE)
            self.inner_sizer.Add(self.device_listbox, (row, 1), flag=wx.EXPAND)
        col = 2 * (device_visible)
        self.inner_sizer.Add(self.flags_lbl, (row, col), flag=LBL_STYLE)
        span = (1, 1) if device_visible else (1, 4)
        self.inner_sizer.Add(self.txt_flags, (row, col + 1), span=span, flag=wx.EXPAND)
        
    def layout_affinity(self, row):
        """Lay out the affinity checkboxes in the specified row."""
        self.inner_sizer.Add(self.affinity_lbl, (row, 0))

        affinity_sizer = wx.BoxSizer(wx.HORIZONTAL)
        # for chk in self.affinity_chks:
            # affinity_sizer.Add(chk)
        # self.inner_sizer.Add(affinity_sizer, (row, 1))

    def layout_balance(self, row):
        """Lay out the balance widgets in the specified row."""
        self.inner_sizer.Add(self.balance_lbl, (row, 0), flag=LBL_STYLE)
        self.inner_sizer.Add(self.balance_amt, (row, 1))

    def layout_finish(self):
        """Lay out the buttons and fit the sizer to the window."""
        self.frame_sizer.Add(self.inner_sizer, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 10)
        self.frame_sizer.Add(self.button_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL)
        self.inner_sizer.AddGrowableCol(1)
        self.inner_sizer.AddGrowableCol(3)
        for btn in [self.start, self.balance_refresh, self.withdraw]:
            self.button_sizer.Add(btn, 0, BTN_STYLE, 5)

        # self.set_widgets_visible([self.external_lbl, self.txt_external],
        #                         self.is_external_miner)
        self.SetSizerAndFit(self.frame_sizer)

    def layout_default(self):
        """Lay out a default miner with no custom changes."""
        
        self.user_lbl.SetLabel(STR_USERNAME)
        self.set_widgets_visible(self.hidden_widgets, False)
        self.set_widgets_visible([self.balance_lbl,
                                  self.balance_amt,
                                  self.balance_refresh,
                                  self.withdraw,
                                  self.server,
                                  self.website,
                                  self.server_lbl,
                                  self.website_lbl,
                                  self.interactive_lbl,
                                  self.txt_interactive,
                                  self.texcache_lbl,
                                  self.txt_texcache,
                                  self.singlemem_lbl,
                                  self.txt_singlemem,
                                  self.warpflags_lbl,
                                  self.txt_warpflags,
                                  self.minercudaminer_lbl], False)
        row = self.layout_init()
        # self.layout_server_and_website(row=row)
        
        customs = ["other", "solo"]
        is_custom = self.server.GetStringSelection().lower() in customs
        if is_custom:
            # self.layout_host_and_port(row=row + 1)
            pass
        # Nope - TT
        #else:
        #    self.set_widgets_visible([self.host_lbl, self.txt_host,
        #                              self.port_lbl, self.txt_port], False)
        
        self.set_widgets_visible([self.affinity_lbl], False)

        self.layout_minertype(row=row)
        self.layout_host_and_port(row=row + 1)
        self.layout_user_and_pass(row=row + 2 + int(is_custom))
        self.layout_device_and_flags(row=row + 3 + int(is_custom))
        self.layout_thrcon_worksize(row=row + 4 + int(is_custom))
        self.layout_vectors_intensity(row=row + 5 + int(is_custom))
        self.layout_gputhreads_gpusettings(row=row + 6 + int(is_custom))
        self.layout_stratum(row=row + 7 + int(is_custom))
        # self.layout_affinity(row=row + 7 + int(is_custom))
        
        if self.external_path == "CGMINER":
            self.set_widgets_visible([self.minerreaper_lbl, self.proxy_lbl], False)
        elif self.external_path == "REAPER":
            self.set_widgets_visible([self.minercgminer_lbl, self.proxy_lbl, self.stratum_lbl, self.txt_stratum, self.flags_lbl, self.txt_flags], False)
        else:
            self.set_widgets_visible([self.minercgminer_lbl, self.minerreaper_lbl], False)
            
        if self.external_path == "PROXY":
            self.set_widgets_visible([self.user_lbl, self.pass_lbl, self.device_lbl, self.thrcon_lbl, self.worksize_lbl, self.vectors_lbl, self.intensity_lbl, self.gputhreads_lbl,  self.gpusettings_lbl,  self.stratum_lbl], False)
            self.set_widgets_visible([self.txt_username, self.txt_pass, self.device_listbox, self.txt_thrcon, self.txt_worksize, self.txt_vectors, self.txt_intensity, self.txt_gputhreads, self.gpusettings, self.txt_stratum], False) 
        else:
            self.set_widgets_visible([self.stratuminfo0_lbl, self.stratuminfo1_lbl], False)
       
        self.layout_finish()

    def layout_interactive_texcache(self, row):
        """
        Interactive and use texture cache for cudaminer
        """
        self.inner_sizer.Add(self.interactive_lbl, (row, 0), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_interactive, (row, 1), flag=wx.EXPAND)
        self.inner_sizer.Add(self.texcache_lbl, (row, 2), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_texcache, (row, 3), flag=wx.EXPAND)
        
    def layout_singlemem_warpflags(self, row):
        """
        Warp configuration string and use single memory block for cudaminer
        """
        self.inner_sizer.Add(self.singlemem_lbl, (row, 0), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_singlemem, (row, 1), flag=wx.EXPAND)
        self.inner_sizer.Add(self.warpflags_lbl, (row, 2), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_warpflags, (row, 3), flag=wx.EXPAND)
        
    def layout_cudaminer(self):
        """Lay out a default miner with no custom changes."""
        
        self.user_lbl.SetLabel(STR_USERNAME)
        self.set_widgets_visible(self.hidden_widgets, False)
        self.set_widgets_visible([self.balance_lbl,
                                  self.balance_amt,
                                  self.balance_refresh,
                                  self.withdraw,
                                  self.server,
                                  self.website,
                                  self.server_lbl,
                                  self.website_lbl,
                                  self.txt_thrcon,
                                  self.thrcon_lbl,
                                  self.txt_worksize,
                                  self.worksize_lbl,
                                  self.txt_vectors,
                                  self.vectors_lbl,
                                  self.txt_intensity,
                                  self.intensity_lbl,
                                  self.txt_gputhreads,
                                  self.gputhreads_lbl,
                                  self.txt_stratum,
                                  self.stratum_lbl,
                                  self.gpusettings,
                                  self.gpusettings_lbl,
                                  self.stratuminfo0_lbl,
                                  self.stratuminfo1_lbl,
                                  self.proxy_lbl,
                                  self.minercgminer_lbl,
                                  self.minerreaper_lbl], False)
                                  
        row = self.layout_init()
        # self.layout_server_and_website(row=row)
        
        customs = ["other", "solo"]
        is_custom = self.server.GetStringSelection().lower() in customs
        if is_custom:
            # self.layout_host_and_port(row=row + 1)
            pass
        # Nope - TT
        #else:
        #    self.set_widgets_visible([self.host_lbl, self.txt_host,
        #                              self.port_lbl, self.txt_port], False)
        
        self.set_widgets_visible([self.affinity_lbl], False)

        self.layout_minertype(row=row)
        self.layout_host_and_port(row=row + 1)
        self.layout_user_and_pass(row=row + 2 + int(is_custom))
        self.layout_device_and_flags(row=row + 3 + int(is_custom))
        self.layout_interactive_texcache(row=row + 4 + int(is_custom))
        self.layout_singlemem_warpflags(row=row + 5 + int(is_custom))

        self.layout_finish()

    ############################
    # Begin server specific code
    def layout_bitpenny(self):
        """BitPenny doesn't require registration or a password.

        The username is just their receiving address.
        """
        invisible = [self.txt_pass, self.txt_host, self.txt_port,
                     self.pass_lbl, self.host_lbl, self.port_lbl]
        self.set_widgets_visible(invisible, False)
        self.set_widgets_visible([self.extra_info], True)

        row = self.layout_init()
        self.layout_server_and_website(row=row)
        self.inner_sizer.Add(self.user_lbl, (row + 1, 0), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_username, (row + 1, 1), span=(1, 3), flag=wx.EXPAND)
        self.layout_device_and_flags(row=row + 2)
        self.layout_affinity(row=row + 3)
        self.layout_balance(row=row + 4)
        self.inner_sizer.Add(self.extra_info, (row + 5, 0), span=(1, 4), flag=wx.ALIGN_CENTER_HORIZONTAL)
        self.layout_finish()

        self.extra_info.SetLabel(_("No registration is required - just enter an address and press Start."))
        self.txt_pass.SetValue('poclbm-gui')
        self.user_lbl.SetLabel(_("Address:"))
        add_tooltip(self.txt_username,
            _("Your receiving address for Bitcoins.\nE.g.: 1A94cjRpaPBMV9ZNWFihB5rTFEeihBALgc"))

    def layout_slush(self):
        """Slush's pool uses a separate username for each miner."""
        self.set_widgets_visible([self.host_lbl, self.txt_host,
                                  self.port_lbl, self.txt_port,
                                  self.withdraw, self.extra_info], False)
        row = self.layout_init()
        self.layout_server_and_website(row=row)
        self.layout_user_and_pass(row=row + 1)
        self.layout_device_and_flags(row=row + 2)
        self.layout_affinity(row=row + 3)
        self.layout_balance(row=row + 4)
        self.layout_finish()

        add_tooltip(self.txt_username,
            _("Your miner username (not your account username).\nExample: Kiv.GPU"))
        add_tooltip(self.txt_pass,
            _("Your miner password (not your account password)."))

    def layout_eligius(self):
        """Eligius doesn't require registration or a password.

        The username is just their receiving address.
        """
        invisible = [self.txt_pass, self.txt_host, self.txt_port,
                     self.withdraw,
                     self.pass_lbl, self.host_lbl, self.port_lbl]
        self.set_widgets_visible(invisible, False)
        self.set_widgets_visible([self.extra_info], True)

        row = self.layout_init()
        self.layout_server_and_website(row=row)
        self.inner_sizer.Add(self.user_lbl, (row + 1, 0), flag=LBL_STYLE)
        self.inner_sizer.Add(self.txt_username, (row + 1, 1), span=(1, 3), flag=wx.EXPAND)
        self.layout_device_and_flags(row=row + 2)
        self.layout_affinity(row=row + 3)
        self.layout_balance(row=row + 4)
        self.inner_sizer.Add(self.extra_info, (row + 5, 0), span=(1, 4), flag=wx.ALIGN_CENTER_HORIZONTAL)
        self.layout_finish()

        self.extra_info.SetLabel(_("No registration is required - just enter an address and press Start."))
        self.txt_pass.SetValue('x')
        self.user_lbl.SetLabel(_("Address:"))
        add_tooltip(self.txt_username,
            _("Your receiving address for Bitcoins.\nE.g.: 1JMfKKJqtkDPbRRsFSLjX1Cs2dqmjKiwj8"))

    def layout_btcguild(self):
        """BTC Guild has the same layout as slush for now."""
        self.layout_slush()

    def layout_bitcoinserver(self):
        """Bitcoin-Server.de has the same layout as slush for now."""
        self.layout_slush()

    def layout_btcmine(self):
        self.set_widgets_visible([self.host_lbl, self.txt_host,
                                  self.port_lbl, self.txt_port,
                                  self.withdraw, self.extra_info], False)
        row = self.layout_init()
        self.layout_server_and_website(row=row)
        self.layout_user_and_pass(row=row + 1)
        self.layout_device_and_flags(row=row + 2)
        self.layout_affinity(row=row + 3)
        self.layout_balance(row=row + 4)
        self.layout_finish()

        add_tooltip(self.txt_username,
            _("Your miner username. \nExample: kiv123@kiv123"))
        add_tooltip(self.txt_pass,
            _("Your miner password (not your account password)."))

    def layout_deepbit(self):
        """Deepbit uses an email address for a username."""
        self.set_widgets_visible([self.host_lbl, self.txt_host,
                                  self.port_lbl, self.txt_port,
                                  self.extra_info], False)
        row = self.layout_init()
        self.layout_server_and_website(row=row)
        self.layout_user_and_pass(row=row + 1)
        self.layout_device_and_flags(row=row + 2)
        self.layout_affinity(row=row + 3)
        self.layout_balance(row=row + 4)
        self.layout_finish()
        add_tooltip(self.txt_username,
            _("The e-mail address you registered with."))
        self.user_lbl.SetLabel(_("Email:"))

    def layout_btcmp(self):
        """Deepbit uses an email address for a username."""
        self.set_widgets_visible([self.host_lbl, self.txt_host,
                                  self.port_lbl, self.txt_port,
                                  self.extra_info], False)
        row = self.layout_init()
        self.layout_server_and_website(row=row)
        self.layout_user_and_pass(row=row + 1)
        self.layout_device_and_flags(row=row + 2)
        self.layout_affinity(row=row + 3)
        self.layout_balance(row=row + 4)
        self.layout_finish()
        add_tooltip(self.txt_username,
            _("Your worker name. Is something in the form of username.workername"))
        self.user_lbl.SetLabel(_("Workername:"))

    def layout_x8s(self):
        """x8s has the same layout as slush for now."""
        self.layout_slush()
    # End server specific code
    ##########################


class GUIMiner(wx.Frame):
    def __init__(self, *args, **kwds):
        wx.Frame.__init__(self, *args, **kwds)
        style = fnb.FNB_X_ON_TAB | fnb.FNB_FF2 | fnb.FNB_HIDE_ON_SINGLE_TAB
        self.nb = fnb.FlatNotebook(self, -1, style=style)

        # Set up notebook context menu
        notebook_menu = wx.Menu()
        ID_RENAME, ID_DUPLICATE = wx.NewId(), wx.NewId()
        notebook_menu.Append(ID_RENAME, _("&Rename..."), _("Rename this miner"))
        notebook_menu.Append(ID_DUPLICATE, _("&Duplicate...", _("Duplicate this miner")))
        self.nb.SetRightClickMenu(notebook_menu)
        self.Bind(wx.EVT_MENU, self.rename_miner, id=ID_RENAME)
        self.Bind(wx.EVT_MENU, self.duplicate_miner, id=ID_DUPLICATE)

        self.console_panel = None
        self.summary_panel = None

        # Servers and defaults are required, it's a fatal error not to have
        # them.
        server_config_path = os.path.join(get_module_path(), 'servers.ini')
        with open(server_config_path) as f:
            data = json.load(f)
            self.servers = data.get('servers')

        defaults_config_path = os.path.join(get_module_path(), 'defaults.ini')
        with open(defaults_config_path) as f:
            self.defaults = json.load(f)
            
        gpusettings_config_path = os.path.join(get_module_path(), 'gpusettings.ini')
        with open(gpusettings_config_path) as f:
            data = json.load(f)
            self.gpusettings_data = data.get('gpusettings')

        self.parse_config()
        self.do_show_opencl_warning = self.config_data.get('show_opencl_warning', True)
        self.console_max_lines = self.config_data.get('console_max_lines', 5000)

        ID_NEW_EXTERNAL, ID_NEW_PHOENIX, ID_NEW_CGMINER, ID_NEW_REAPER, ID_NEW_CUDAMINER, ID_NEW_PROXY, ID_NEW_UFASOFT = wx.NewId(), wx.NewId(), wx.NewId(), wx.NewId(), wx.NewId(), wx.NewId(), wx.NewId()
        self.menubar = wx.MenuBar()
        file_menu = wx.Menu()
        new_menu = wx.Menu()
        #new_menu.Append(wx.ID_NEW, _("&New OpenCL miner..."), _("Create a new OpenCL miner (default for ATI cards)"), wx.ITEM_NORMAL)
        #new_menu.Append(ID_NEW_PHOENIX, _("New Phoenix miner..."), _("Create a new Phoenix miner (for some ATI cards)"), wx.ITEM_NORMAL)
        new_menu.Append(ID_NEW_CGMINER, _("New CG miner..."), _("Create a new CGMiner"), wx.ITEM_NORMAL)
        new_menu.Append(ID_NEW_REAPER, _("New reaper miner..."), _("Create a new reaper miner"), wx.ITEM_NORMAL)
        new_menu.Append(ID_NEW_CUDAMINER, _("New CUDA miner..."), _("Create a new CUDA miner"), wx.ITEM_NORMAL)
        new_menu.Append(ID_NEW_PROXY, _("New stratum proxy..."), _("Create a new stratum proxy"), wx.ITEM_NORMAL)
        #new_menu.Append(ID_NEW_UFASOFT, _("New Ufasoft CPU miner..."), _("Create a new Ufasoft miner (for CPUs)"), wx.ITEM_NORMAL)
        #new_menu.Append(ID_NEW_EXTERNAL, _("New &other miner..."), _("Create a new custom miner (requires external program)"), wx.ITEM_NORMAL)
        file_menu.AppendMenu(wx.NewId(), _('&New miner'), new_menu)
        file_menu.Append(wx.ID_SAVE, _("&Save settings"), _("Save your settings"), wx.ITEM_NORMAL)
        file_menu.Append(wx.ID_OPEN, _("&Load settings"), _("Load stored settings"), wx.ITEM_NORMAL)
        file_menu.Append(wx.ID_EXIT, _("Quit"), STR_QUIT, wx.ITEM_NORMAL)
        self.menubar.Append(file_menu, _("&File"))

        ID_SUMMARY, ID_CONSOLE = wx.NewId(), wx.NewId()
        view_menu = wx.Menu()
        view_menu.Append(ID_SUMMARY, _("Show summary"), _("Show summary of all miners"), wx.ITEM_NORMAL)
        view_menu.Append(ID_CONSOLE, _("Show console"), _("Show console logs"), wx.ITEM_NORMAL)
        self.menubar.Append(view_menu, _("&View"))

        ID_SOLO, ID_PATHS, ID_BLOCKCHAIN_PATH, ID_LAUNCH = wx.NewId(), wx.NewId(), wx.NewId(), wx.NewId()
        solo_menu = wx.Menu()
        solo_menu.Append(ID_SOLO, _("&Create solo password..."), _("Configure a user/pass for solo mining"), wx.ITEM_NORMAL)
        solo_menu.Append(ID_PATHS, _("&Set Bitcoin client path..."), _("Set the location of the official Bitcoin client"), wx.ITEM_NORMAL)
        solo_menu.Append(ID_BLOCKCHAIN_PATH, _("&Set Bitcoin data directory..."), _("Set the location of the bitcoin data directory containing the blockchain and wallet"), wx.ITEM_NORMAL)
        solo_menu.Append(ID_LAUNCH, _("&Launch Bitcoin client as server"), _("Launch the official Bitcoin client as a server for solo mining"), wx.ITEM_NORMAL)
        self.menubar.Append(solo_menu, _("&Solo utilities"))

        ID_START_MINIMIZED = wx.NewId()
        self.options_menu = wx.Menu()
        self.start_minimized_chk = self.options_menu.Append(ID_START_MINIMIZED, _("Start &minimized"), _("Start the GUI minimized to the tray."), wx.ITEM_CHECK)
        self.options_menu.Check(ID_START_MINIMIZED, self.config_data.get('start_minimized', False))
        self.menubar.Append(self.options_menu, _("&Options"))

        ID_CHANGE_LANGUAGE = wx.NewId()
        lang_menu = wx.Menu()
        lang_menu.Append(ID_CHANGE_LANGUAGE, _("&Change language..."), "", wx.ITEM_NORMAL)
        self.menubar.Append(lang_menu, _("Language"))

        ID_DONATE_SMALL = wx.NewId()
        donate_menu = wx.Menu()
        donate_menu.Append(ID_DONATE_SMALL, _("&Donate..."), _("Donate Litecoins to support GUIMiner development"))
        self.menubar.Append(donate_menu, _("&Donate"))

        help_menu = wx.Menu()
        help_menu.Append(wx.ID_ABOUT, _("&About..."), STR_ABOUT, wx.ITEM_NORMAL)

        self.menubar.Append(help_menu, _("&Help"))
        self.SetMenuBar(self.menubar)
        self.statusbar = self.CreateStatusBar(2, 0)

        try:
            self.bitcoin_executable = os.path.join(os.getenv("PROGRAMFILES"), "Bitcoin", "bitcoin-qt.exe")
        except:
            self.bitcoin_executable = "" # TODO: where would Bitcoin probably be on Linux/Mac?

        try:
            self.blockchain_directory = os.path.join(os.getenv("APPDATA"), "Bitcoin")
        except:
            self.blockchain_directory = ""
          
        
        try:
            self.tbicon = GUIMinerTaskBarIcon(self)
        except:
            logging.error(_("Failed to load taskbar icon; continuing."))

        self.set_properties()

        try:
            self.devices = get_opencl_devices()
        except:
            self.devices = []
            file_menu.Enable(wx.ID_NEW, False)
            file_menu.SetHelpString(wx.ID_NEW, _("OpenCL not found - can't add a OpenCL miner"))

            if self.do_show_opencl_warning:
                dialog = OpenCLWarningDialog(self)
                dialog.ShowModal()
                self.do_show_opencl_warning = not dialog.is_box_checked()

        self.Bind(wx.EVT_MENU, self.name_new_profile, id=wx.ID_NEW)
        #self.Bind(wx.EVT_MENU, self.new_phoenix_profile, id=ID_NEW_PHOENIX)
        self.Bind(wx.EVT_MENU, self.new_cgminer_profile, id=ID_NEW_CGMINER)
        self.Bind(wx.EVT_MENU, self.new_ufasoft_profile, id=ID_NEW_UFASOFT)
        self.Bind(wx.EVT_MENU, self.new_reaper_profile, id=ID_NEW_REAPER)
        self.Bind(wx.EVT_MENU, self.new_cudaminer_profile, id=ID_NEW_CUDAMINER)
        self.Bind(wx.EVT_MENU, self.new_proxy_profile, id=ID_NEW_PROXY)
        self.Bind(wx.EVT_MENU, self.new_external_profile, id=ID_NEW_EXTERNAL)
        self.Bind(wx.EVT_MENU, self.save_config, id=wx.ID_SAVE)
        self.Bind(wx.EVT_MENU, self.load_config, id=wx.ID_OPEN)
        self.Bind(wx.EVT_MENU, self.on_menu_exit, id=wx.ID_EXIT)
        self.Bind(wx.EVT_MENU, self.set_official_client_path, id=ID_PATHS)
        self.Bind(wx.EVT_MENU, self.set_blockchain_directory, id=ID_BLOCKCHAIN_PATH)
        self.Bind(wx.EVT_MENU, self.show_console, id=ID_CONSOLE)
        self.Bind(wx.EVT_MENU, self.show_summary, id=ID_SUMMARY)
        self.Bind(wx.EVT_MENU, self.show_about_dialog, id=wx.ID_ABOUT)
        self.Bind(wx.EVT_MENU, self.create_solo_password, id=ID_SOLO)
        self.Bind(wx.EVT_MENU, self.launch_solo_server, id=ID_LAUNCH)
        self.Bind(wx.EVT_MENU, self.on_change_language, id=ID_CHANGE_LANGUAGE)
        self.Bind(wx.EVT_MENU, self.on_donate, id=ID_DONATE_SMALL)
        self.Bind(wx.EVT_CLOSE, self.on_close)        
        self.Bind(wx.EVT_ICONIZE, self.on_iconize)
        self.Bind(fnb.EVT_FLATNOTEBOOK_PAGE_CLOSING, self.on_page_closing)
        self.Bind(fnb.EVT_FLATNOTEBOOK_PAGE_CLOSED, self.on_page_closed)
        self.Bind(fnb.EVT_FLATNOTEBOOK_PAGE_CHANGED, self.on_page_changed)

        self.load_config()
        self.do_layout()

        if not self.start_minimized_chk.IsChecked():
            self.Show()
            
    def on_iconize(self, event):
        if event.Iconized() and sys.platform == 'win32':
            self.Hide() # On minimize, hide from taskbar.
        else:
            self.Show()

    def set_properties(self):
        self.SetIcons(get_icon_bundle())
        self.SetTitle(_("GUIMiner-scrypt alpha"))
        self.statusbar.SetStatusWidths([-1, 125])
        statusbar_fields = ["", STR_NOT_STARTED]
        for i in range(len(statusbar_fields)):
            self.statusbar.SetStatusText(statusbar_fields[i], i)

    def do_layout(self):
        self.vertical_sizer = wx.BoxSizer(wx.VERTICAL)
        self.vertical_sizer.Add(self.nb, 1, wx.EXPAND, 20)
        self.SetSizer(self.vertical_sizer)
        self.vertical_sizer.SetSizeHints(self)
        self.SetSizerAndFit(self.vertical_sizer)
        self.Layout()

    @property
    def profile_panels(self):
        """Return a list of currently available MinerTab."""
        pages = [self.nb.GetPage(i) for i in range(self.nb.GetPageCount())]
        return [p for p in pages if
                p != self.console_panel and p != self.summary_panel]

    def add_profile(self, data={}):
        """Add a new MinerTab to the list of tabs."""
        panel = MinerTab(self.nb, -1, self.devices, self.servers,
                             self.defaults, self.gpusettings_data, self.statusbar, data)
        self.nb.AddPage(panel, panel.name)
        # The newly created profile should have focus.
        self.nb.EnsureVisible(self.nb.GetPageCount() - 1)

        if self.summary_panel is not None:
            self.summary_panel.add_miners_to_grid() # Show new entry on summary
        return panel

    def message(self, *args, **kwargs):
        """Utility method to show a message dialog and return their choice."""
        dialog = wx.MessageDialog(self, *args, **kwargs)
        retval = dialog.ShowModal()
        dialog.Destroy()
        return retval

    def name_new_profile(self, event=None, extra_profile_data={}):
        """Prompt for the new miner's name."""
        dialog = wx.TextEntryDialog(self, _("Name this miner:"), _("New miner"))
        if dialog.ShowModal() == wx.ID_OK:
            name = dialog.GetValue().strip()
            if not name: name = _("Untitled")
            data = extra_profile_data.copy()
            data['name'] = name
            self.add_profile(data)

    def new_external_profile(self, event):
        """Prompt for an external miner path, then create a miner.

        On Windows we validate against legal miners; on Linux they can pick
        whatever they want.
        """
        wildcard = _('External miner (*.exe)|*.exe|(*.py)|*.py') if sys.platform == 'win32' else '*.*'
        dialog = wx.FileDialog(self,
                               _("Select external miner:"),
                               defaultDir=os.path.join(get_module_path(), 'miners'),
                               defaultFile="",
                               wildcard=wildcard,
                               style=wx.OPEN)
        if dialog.ShowModal() != wx.ID_OK:
            return

        if sys.platform == 'win32' and dialog.GetFilename() not in SUPPORTED_BACKENDS:
            self.message(
                _("Unsupported external miner %(filename)s. Supported are: %(supported)s") % \
                  dict(filename=dialog.GetFilename(), supported='\n'.join(SUPPORTED_BACKENDS)),
                _("Miner not supported"), wx.OK | wx.ICON_ERROR)
            return
        path = os.path.join(dialog.GetDirectory(), dialog.GetFilename())
        dialog.Destroy()
        self.name_new_profile(extra_profile_data=dict(external_path="CGMINER"))

    def new_phoenix_profile(self, event):
        """Create a new miner using the Phoenix OpenCL miner backend."""
        path = os.path.join(get_module_path(), 'phoenix.exe')
        self.name_new_profile(extra_profile_data=dict(external_path="CGMINER"))

    def new_cgminer_profile(self, event):
        """Create a new miner using the Cgminer OpenCL miner backend."""
        path = os.path.join(get_module_path(), 'cgminer.exe')
        self.name_new_profile(extra_profile_data=dict(external_path="CGMINER"))

    def new_ufasoft_profile(self, event):
        """Create a new miner using the Ufasoft CPU miner backend."""
        # path = os.path.join(get_module_path(), 'miners', 'ufasoft', 'bitcoin-miner.exe')
        self.name_new_profile(extra_profile_data=dict(external_path="CGMINER"))

    def new_reaper_profile(self, event):
        """Create a new miner using the REAPER GPU miner backend."""
        # path = os.path.join(get_module_path(), 'miners', 'puddinpop', 'rpcminer-cuda.exe')
        self.name_new_profile(extra_profile_data=dict(external_path="REAPER"))
        
    def new_cudaminer_profile(self, event):
        """Create a new miner using the cudaminer GPU miner backend."""
        self.name_new_profile(extra_profile_data=dict(external_path="CUDAMINER"))
        
    def new_proxy_profile(self, event):
        """Create a stratum proxy backend."""
        # path = os.path.join(get_module_path(), 'miners', 'puddinpop', 'rpcminer-cuda.exe')
        self.name_new_profile(extra_profile_data=dict(external_path="PROXY"))

    def get_storage_location(self):
        """Get the folder and filename to store our JSON config."""
        if sys.platform == 'win32':
            folder = os.path.join(os.environ['AppData'], 'poclbm')
            config_filename = os.path.join(folder, 'poclbm_scrypt.ini')
        else: # Assume linux? TODO test
            folder = os.environ['HOME']
            config_filename = os.path.join(folder, '.poclbm')
        return folder, config_filename

    def on_close(self, event):
        """Minimize to tray if they click "close" but exit otherwise.

        On closing, stop any miners that are currently working.
        """
        if event.CanVeto():
            self.Hide()
            event.Veto()
        else:
            if any(p.is_modified for p in self.profile_panels):
                dialog = wx.MessageDialog(self, _('Do you want to save changes?'), _('Save'),
                    wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
                retval = dialog.ShowModal()
                dialog.Destroy()
                if retval == wx.ID_YES:
                    self.save_config()

            if self.console_panel is not None:
                self.console_panel.on_close()
            if self.summary_panel is not None:
                self.summary_panel.on_close()
            for p in self.profile_panels:
                p.on_close()
            if self.tbicon is not None:
                self.tbicon.RemoveIcon()
                self.tbicon.timer.Stop()
                self.tbicon.Destroy()
            event.Skip()

    def save_config(self, event=None):
        """Save the current miner profiles to our config file in JSON format."""
        folder, config_filename = self.get_storage_location()
        mkdir_p(folder)
        profile_data = [p.get_data() for p in self.profile_panels]
        config_data = dict(show_console=self.is_console_visible(),
                           show_summary=self.is_summary_visible(),
                           profiles=profile_data,
                           bitcoin_executable=self.bitcoin_executable,
                           blockchain_directory=self.blockchain_directory,
                           show_opencl_warning=self.do_show_opencl_warning,
                           start_minimized=self.start_minimized_chk.IsChecked(),
                           console_max_lines=self.console_max_lines,
                           window_position=list(self.GetRect()))
        logger.debug(_('Saving: ') + json.dumps(config_data))
        try:
            with open(config_filename, 'w') as f:
                json.dump(config_data, f, indent=4)
        except IOError:
            self.message(
                _("Couldn't write save file %s.\nCheck the location is writable.") % config_filename,
                _("Save unsuccessful"), wx.OK | wx.ICON_ERROR)
        else:
            self.message(_("Profiles saved OK to %s.") % config_filename,
                      _("Save successful"), wx.OK | wx.ICON_INFORMATION)
            for p in self.profile_panels:
                p.on_saved()

    def parse_config(self):
        """Set self.config_data to a dictionary of config values."""
        self.config_data = {}

        try:
            config_filename = self.get_storage_location()[1]
            if os.path.exists(config_filename):
                with open(config_filename) as f:
                    self.config_data.update(json.load(f))
                logger.debug(_('Loaded: %s') % json.dumps(self.config_data))
        except ValueError:
            self.message(
                _("Your settings saved at:\n %s\nare corrupt or could not be read.\nDeleting this file or saving over it may solve the problem." % config_filename),
                _("Error"), wx.ICON_ERROR)

    def load_config(self, event=None):
        """Load JSON profile info from the config file."""
        self.parse_config()

        config_data = self.config_data
        executable = config_data.get('bitcoin_executable', None)
        if executable is not None:
            self.bitcoin_executable = executable
            
        blockchain_directory = config_data.get('blockchain_directory', None)
        if blockchain_directory is not None:
            self.blockchain_directory = blockchain_directory

        # Shut down any existing miners before they get clobbered
        if(any(p.is_mining for p in self.profile_panels)):
            result = self.message(
                _("Loading profiles will stop any currently running miners. Continue?"),
                _("Load profile"), wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION)
            if result == wx.ID_NO:
                return
        for p in reversed(self.profile_panels):
            p.on_close()
            self.nb.DeletePage(self.nb.GetPageIndex(p))

        # If present, summary should be the leftmost tab on startup.
        if config_data.get('show_summary', False):
            self.show_summary()

        profile_data = config_data.get('profiles', [])
        for d in profile_data:
            self.add_profile(d)

        if not any(profile_data):
            self.add_profile() # Create a default one using defaults.ini

        if config_data.get('show_console', False):
            self.show_console()
            
        window_position = config_data.get('window_position')
        if window_position:
            self.SetRect(window_position)

        for p in self.profile_panels:
            if p.autostart:
                p.start_mining()

    def set_official_client_path(self, event):
        """Set the path to the official Bitcoin client."""
        wildcard = "*.exe" if sys.platform == 'win32' else '*.*'
        dialog = wx.FileDialog(self,
                               _("Select path to Bitcoin.exe"),
                               defaultFile="bitcoin-qt.exe",
                               wildcard=wildcard,
                               style=wx.OPEN)
        if dialog.ShowModal() == wx.ID_OK:
            path = os.path.join(dialog.GetDirectory(), dialog.GetFilename())
            if os.path.exists(path):
                self.bitcoin_executable = path
        dialog.Destroy()
        
    def set_blockchain_directory(self, event):
        """Set the path to the blockchain data directory."""
        defaultPath = os.path.join(os.getenv("APPDATA"), "Bitcoin")
        dialog = wx.DirDialog(self,
                              _("Select path to blockchain"),
                              defaultPath=defaultPath,
                              style=wx.DD_DIR_MUST_EXIST)
        if dialog.ShowModal() == wx.ID_OK:
            path = dialog.GetPath()
            if os.path.exists(path):
                self.blockchain_directory = path
        dialog.Destroy()   

    def show_about_dialog(self, event):
        """Show the 'about' dialog."""
        dialog = AboutGuiminer(self, -1, _('About'))
        dialog.ShowModal()
        dialog.Destroy()

    def on_page_closing(self, event):
        """Handle a tab closing event.

        If they are closing a special panel, we have to shut it down.
        If the tab has a miner running in it, we have to stop the miner
        before letting the tab be removed.
        """
        p = self.nb.GetPage(event.GetSelection())

        if p == self.console_panel:
            self.console_panel.on_close()
            self.console_panel = None
            event.Skip()
            return
        if p == self.summary_panel:
            self.summary_panel.on_close()
            self.summary_panel = None
            event.Skip()
            return

        if p.is_mining:
            result = self.message(
                _("Closing this miner will stop it. Continue?"),
                _("Close miner"),
                wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION)
            if result == wx.ID_NO:
                event.Veto()
                return
        p.on_close()
        event.Skip() # OK to close the tab now

    def on_page_closed(self, event):
        if self.summary_panel is not None:
            self.summary_panel.add_miners_to_grid() # Remove miner summary

    def on_page_changed(self, event):
        """Handle a tab change event.

        Ensures the status bar shows the status of the tab that has focus.
        """
        p = self.nb.GetPage(event.GetSelection())
        p.on_focus()

    def launch_solo_server(self, event):
        """Launch the official bitcoin client in server mode.

        This allows poclbm to connect to it for mining solo.
        """
        if self.blockchain_directory and os.path.exists(self.blockchain_directory):
            datadir = " -datadir=%s" % self.blockchain_directory
        else:
            datadir = ""
        try:
            subprocess.Popen(self.bitcoin_executable + " -server" + datadir)
        except OSError:
            self.message(
                _("Couldn't find Bitcoin at %s. Is your path set correctly?") % self.bitcoin_executable,
                _("Launch failed"), wx.ICON_ERROR | wx.OK)
            return
        self.message(
            _("The Bitcoin client will now launch in server mode.\nOnce it connects to the network and downloads the block chain, you can start a miner in 'solo' mode."),
            _("Launched ok."),
            wx.OK)

    def create_solo_password(self, event):
        """Prompt the user for login credentials to the bitcoin client.

        These are required to connect to the client over JSON-RPC and are
        stored in 'bitcoin.conf'.
        """
        if sys.platform == 'win32':
            filename = os.path.join(os.getenv("APPDATA"), "Bitcoin", "bitcoin.conf")
        else: # Assume Linux for now TODO test
            filename = os.path.join(os.getenv('HOME'), ".bitcoin")
        if os.path.exists(filename):
            result = self.message(
                _("%s already exists. Overwrite?") % filename,
                _("bitcoin.conf already exists."),
                wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION)
            if result == wx.ID_NO:
                return

        dialog = SoloPasswordRequest(self, _('Enter password'))
        result = dialog.ShowModal()
        dialog.Destroy()
        if result == wx.ID_CANCEL:
            return

        with open(filename, "w") as f:
            f.write('\nrpcuser=%s\nrpcpassword=%s\nrpcallowip=*' % dialog.get_value())
            f.close()

        self.message(_("Wrote bitcoin config ok."), _("Success"), wx.OK)

    def is_console_visible(self):
        """Return True if the console is visible."""
        return self.nb.GetPageIndex(self.console_panel) != -1

    def show_console(self, event=None):
        """Show the console log in its own tab."""
        if self.is_console_visible():
            return # Console already shown
        self.console_panel = ConsolePanel(self, self.console_max_lines)
        self.nb.AddPage(self.console_panel, _("Console"))
        self.nb.EnsureVisible(self.nb.GetPageCount() - 1)

    def is_summary_visible(self):
        """Return True if the summary is visible."""
        return self.nb.GetPageIndex(self.summary_panel) != -1

    def show_summary(self, event=None):
        """Show the summary window in its own tab."""
        if self.is_summary_visible():
            return
        self.summary_panel = SummaryPanel(self)
        self.nb.AddPage(self.summary_panel, _("Summary"))
        index = self.nb.GetPageIndex(self.summary_panel)
        self.nb.SetSelection(index)

    def on_menu_exit(self, event):
        self.Close(force=True)

    def rename_miner(self, event):
        """Change the name of a miner as displayed on the tab."""
        p = self.nb.GetPage(self.nb.GetSelection())
        if p not in self.profile_panels:
            return

        dialog = wx.TextEntryDialog(self, _("Rename to:"), _("Rename miner"))
        if dialog.ShowModal() == wx.ID_OK:
            p.set_name(dialog.GetValue().strip())

    def duplicate_miner(self, event):
        """Duplicate the current miner to another miner."""
        p = self.nb.GetPage(self.nb.GetSelection())
        if p not in self.profile_panels:
            return        
        self.name_new_profile(event=None, extra_profile_data=p.get_data())

    def on_change_language(self, event):
        dialog = ChangeLanguageDialog(self, _('Change language'), language)
        result = dialog.ShowModal()
        dialog.Destroy()
        if result == wx.ID_CANCEL:
            return

        language_name = dialog.get_value()
        update_language(LANGUAGES[language_name])
        save_language()

    def on_donate(self, event):
        dialog = DonateDialog(self, -1, _('Donate'))
        dialog.ShowModal()
        dialog.Destroy()

class DonateDialog(wx.Dialog):
    """About dialog for the app with a donation address."""
    DONATE_TEXT = "If this software helped you, please consider contributing to its development." \
                  "\nSend donations to:  %(address)s"
    def __init__(self, parent, id, title):
        wx.Dialog.__init__(self, parent, id, title)
        vbox = wx.BoxSizer(wx.VERTICAL)

        text = DonateDialog.DONATE_TEXT % dict(address=DONATION_ADDRESS)
        self.about_text = wx.StaticText(self, -1, text)
        self.copy_btn = wx.Button(self, -1, _("Copy address to clipboard"))
        vbox.Add(self.about_text, 0, wx.ALL, 10)
        vbox.Add(self.copy_btn, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 10)
        self.SetSizerAndFit(vbox)

        self.copy_btn.Bind(wx.EVT_BUTTON, self.on_copy)

    def on_copy(self, event):
        """Copy the donation address to the clipboard."""
        if wx.TheClipboard.Open():
            data = wx.TextDataObject()
            data.SetText(DONATION_ADDRESS)
            wx.TheClipboard.SetData(data)
        wx.TheClipboard.Close()
        

class ChangeLanguageDialog(wx.Dialog):
    """Dialog prompting the user to change languages."""
    def __init__(self, parent, title, current_language):
        style = wx.DEFAULT_DIALOG_STYLE
        vbox = wx.BoxSizer(wx.VERTICAL)
        wx.Dialog.__init__(self, parent, -1, title, style=style)
        self.lbl = wx.StaticText(self, -1,
            _("Choose language (requires restart to take full effect)"))
        vbox.Add(self.lbl, 0, wx.ALL, 10)
        self.language_choices = wx.ComboBox(self, -1,
                                            choices=sorted(LANGUAGES.keys()),
                                            style=wx.CB_READONLY)

        self.language_choices.SetStringSelection(LANGUAGES_REVERSE[current_language])

        vbox.Add(self.language_choices, 0, wx.ALL, 10)
        buttons = self.CreateButtonSizer(wx.OK | wx.CANCEL)
        vbox.Add(buttons, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 10)
        self.SetSizerAndFit(vbox)

    def get_value(self):
        return self.language_choices.GetStringSelection()


class SoloPasswordRequest(wx.Dialog):
    """Dialog prompting user for login credentials for solo mining."""
    def __init__(self, parent, title):
        style = wx.DEFAULT_DIALOG_STYLE
        vbox = wx.BoxSizer(wx.VERTICAL)
        wx.Dialog.__init__(self, parent, -1, title, style=style)
        self.user_lbl = wx.StaticText(self, -1, STR_USERNAME)
        self.txt_username = wx.TextCtrl(self, -1, "")
        self.pass_lbl = wx.StaticText(self, -1, STR_PASSWORD)
        self.txt_pass = wx.TextCtrl(self, -1, "", style=wx.TE_PASSWORD)
        grid_sizer_1 = wx.FlexGridSizer(2, 2, 5, 5)
        grid_sizer_1.Add(self.user_lbl, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 0)
        grid_sizer_1.Add(self.txt_username, 0, wx.EXPAND, 0)
        grid_sizer_1.Add(self.pass_lbl, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 0)
        grid_sizer_1.Add(self.txt_pass, 0, wx.EXPAND, 0)
        buttons = self.CreateButtonSizer(wx.OK | wx.CANCEL)
        vbox.Add(grid_sizer_1, wx.EXPAND | wx.ALL, 10)
        vbox.Add(buttons)
        self.SetSizerAndFit(vbox)

    def get_value(self):
        """Return the (username, password) supplied by the user."""
        return self.txt_username.GetValue(), self.txt_pass.GetValue()


class BalanceAuthRequest(wx.Dialog):
    """Dialog prompting user for an auth token to refresh their balance."""
    instructions = \
_("""Click the link below to log in to the pool and get a special token.
This token lets you securely check your balance.
To remember this token for the future, save your miner settings.""")
    def __init__(self, parent, url):
        style = wx.DEFAULT_DIALOG_STYLE
        vbox = wx.BoxSizer(wx.VERTICAL)
        wx.Dialog.__init__(self, parent, -1, STR_REFRESH_BALANCE, style=style)
        self.instructions = wx.StaticText(self, -1, BalanceAuthRequest.instructions)
        self.website = hyperlink.HyperLinkCtrl(self, -1, url)
        self.txt_token = wx.TextCtrl(self, -1, _("(Paste token here)"))
        buttons = self.CreateButtonSizer(wx.OK | wx.CANCEL)

        vbox.AddMany([
            (self.instructions, 0, wx.ALL, 10),
            (self.website, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 10),
            (self.txt_token, 0, wx.EXPAND | wx.ALIGN_CENTER_HORIZONTAL, 10),
            (buttons, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 10)
        ])
        self.SetSizerAndFit(vbox)

    def get_value(self):
        """Return the auth token supplied by the user."""
        return self.txt_token.GetValue()


class AboutGuiminer(wx.Dialog):
    """About dialog for the app with a donation address."""
    
    def __init__(self, parent, id, title):
        wx.Dialog.__init__(self, parent, id, title)
        vbox = wx.BoxSizer(wx.VERTICAL)

        text = ABOUT_TEXT % dict(version=__version__,
                                 address=DONATION_ADDRESS)
        self.about_text = wx.StaticText(self, -1, text)
        self.copy_btn = wx.Button(self, -1, _("Copy address to clipboard"))
        vbox.Add(self.about_text)
        vbox.Add(self.copy_btn, 0, wx.ALIGN_BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 0)
        self.SetSizerAndFit(vbox)

        self.copy_btn.Bind(wx.EVT_BUTTON, self.on_copy)

    def on_copy(self, event):
        """Copy the donation address to the clipboard."""
        if wx.TheClipboard.Open():
            data = wx.TextDataObject()
            data.SetText(DONATION_ADDRESS)
            wx.TheClipboard.SetData(data)
        wx.TheClipboard.Close()


class OpenCLWarningDialog(wx.Dialog):
    """Warning dialog when a user does not have OpenCL installed."""
    def __init__(self, parent):
        wx.Dialog.__init__(self, parent, -1, _("No OpenCL devices found."))
        vbox = wx.BoxSizer(wx.VERTICAL)
        self.message = wx.StaticText(self, -1,
 _("""No OpenCL devices were found.
 If you only want to mine using CPU or CUDA, you can ignore this message.
 If you want to mine on ATI graphics cards, you may need to install the ATI Stream
 SDK, or your GPU may not support OpenCL."""))
        vbox.Add(self.message, 0, wx.ALL, 10)

        hbox = wx.BoxSizer(wx.HORIZONTAL)

        self.no_show_chk = wx.CheckBox(self, -1)
        hbox.Add(self.no_show_chk)
        self.no_show_txt = wx.StaticText(self, -1, _("Don't show this message again"))
        hbox.Add((5, 0))
        hbox.Add(self.no_show_txt)
        vbox.Add(hbox, 0, wx.ALL, 10)
        buttons = self.CreateButtonSizer(wx.OK)
        vbox.Add(buttons, 0, wx.ALIGN_BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 0)
        self.SetSizerAndFit(vbox)

    def is_box_checked(self):
        return self.no_show_chk.GetValue()


def run():
    try:
        frame_1 = GUIMiner(None, -1, "")
        app.SetTopWindow(frame_1)
        app.MainLoop()
    except:
        logging.exception("Exception:")
        raise


if __name__ == "__main__":
    run()