
// initialize menus & dialogs, etc.
// for most of the prefs dialogs, all code resides here in the form of
// event handlers & validators
// other non-viewer dialogs are at least validated enough that they won't crash
// viewer dialogs are not commonly used, so they are initialized on demand

#include "wxvbam.h"

#include <wx/stockitem.h>
#include <wx/spinctrl.h>
#include <wx/clrpicker.h>
#include <wx/filepicker.h>
#include <wx/dir.h>
#include <wx/tokenzr.h>
#include "wx/checkedlistctrl.h"
#include <wx/progdlg.h>
#include <algorithm>
#include "../gba/CheatSearch.h"

// The program icon, in case it's missing from .xrc (MSW gets it from .rc file)
#if !defined(__WXMSW__) && !defined(__WXPM__)
// ImageMagick makes the name wxvbam, but wx expects wxvbam_xpm
#define wxvbam wxvbam_xpm
const
#include "wxvbam.xpm"
#undef wxvbam
#endif

// this is supposed to happen automatically if a parent is marked recursive
// but some dialogs don't do it (propertydialog?)
// so go ahead and mark all dialogs for fully recursive validation
static void mark_recursive(wxWindowBase *w)
{
    w->SetExtraStyle(w->GetExtraStyle() | wxWS_EX_VALIDATE_RECURSIVELY);
    wxWindowList l = w->GetChildren();
    for(wxWindowList::iterator ch = l.begin(); ch != l.end(); ch++)
	mark_recursive(*ch);
}

#define GetXRCDialog(n) \
    wxStaticCast(wxGetApp().frame->FindWindow(XRCID(n)), wxDialog)

// Event handlers must be methods of wxEvtHandler-derived objects

// manage the network link dialog
#ifndef NO_LINK
static class NetLink_t : public wxEvtHandler
{
public:
    wxDialog *dlg;
    int n_players;
    NetLink_t() : n_players(2) {}
    wxButton *okb;
    void ServerOKButton(wxCommandEvent &ev)
    {
	okb->SetLabel(_("Start!"));
    }
    void ClientOKButton(wxCommandEvent &ev)
    {
	okb->SetLabel(_("Connect"));
    }
    // attached to OK, so skip when OK
    void NetConnect(wxCommandEvent &ev)
    {
	if(!dlg->Validate() || !dlg->TransferDataFromWindow())
	    return;
	update_opts(); // save fast flag and client host

	wxString connmsg, pmsg;

	wxMutex lock;
	wxCondition sig(lock);
	lock.Lock();

	bool done = false;

	if(lanlink.server) {
	    lanlink.numslaves = n_players - 1;
	    class sid_t : public ServerInfoDisplay
	    {
		wxMutex *lock;
		wxCondition *sig;
		wxString *connmsg, *pmsg;
		bool *done;
		bool conn[3];
	    public:
		sid_t(wxMutex *m, wxCondition *c, wxString *cm, wxString *pm,
		      bool *d) :
		    lock(m), sig(c), connmsg(cm), pmsg(pm), done(d) {}
		void ShowServerIP(const sf::IPAddress &addr) {
		    wxString addr_s(addr.ToString().c_str(), wxConvLibc);
		    wxString msg;
		    msg.Printf(_("Server IP address is: %s\n"), addr_s.c_str());
		    connmsg->append(msg);
		    conn[0] = conn[1] = conn[2] = false;
		}
		void ShowConnect(int player) {
		    wxString msg;
		    conn[player - 1] = true;
		    lock->Lock();
		    pmsg->clear();
		    for(int i = 0; i < 3; i++)
			if(conn[i]) {
			    msg.Printf(_("Player %d connected\n"), i + 2);
			    pmsg->append(msg);
			}
		    sig->Signal();
		    lock->Unlock();
		}
		void Ping() {
		    lock->Lock();
		    sig->Signal();
		    if(*done)
			lanlink.terminate = true;
		    lock->Unlock();
		}
		void Connected() {
		    lock->Lock();
		    *done = true;
		    sig->Signal();
		    lock->Unlock();
		}
	    };
	    
	    sid_t* sid = new sid_t(&lock, &sig, &connmsg, &pmsg, &done);
	    
	    if (!ls.Init(sid)) {
			wxLogError(_("Error occurred.\nPlease try again."));
			lock.Unlock();
			delete sid;
			return;
	    }
	    
	    wxProgressDialog
		pdlg(_("Waiting for clients..."), connmsg,
		     100, dlg, wxPD_APP_MODAL|wxPD_CAN_ABORT|wxPD_ELAPSED_TIME);
		     
	    while(!done) {
			if(!pdlg.Pulse(connmsg + pmsg))
				done = true;
			sig.Wait();
	    }
	} else {
	    class cid_t : public ClientInfoDisplay
	    {
		wxMutex *lock;
		wxCondition *sig;
		wxString *connmsg, *pmsg;
		bool *done;
	    public:
		cid_t(wxMutex *m, wxCondition *c, wxString *cm, wxString *pm,
		      bool *d) :
		    lock(m), sig(c), connmsg(cm), pmsg(pm), done(d) {}
		void ConnectStart(const sf::IPAddress &addr) {
		    wxString addr_s(addr.ToString().c_str(), wxConvLibc);
		    connmsg->Printf(_("Connecting to %s\n"), addr_s.c_str());
		}
		void ShowConnect(int player, int togo) {
		    wxString msg;
		    lock->Lock();
		    pmsg->Printf(_("Connected as #%d\n"), player);
		    if(togo)
			msg.Printf(_("Waiting for %d players to join"), togo);
		    else
			msg = _("All players joined.");
		    pmsg->append(msg);
		    sig->Signal();
		    lock->Unlock();
		}
		void Ping() {
		    lock->Lock();
		    sig->Signal();
		    if(*done)
			lanlink.terminate = true;
		    lock->Unlock();
		}
		void Connected() {
		    lock->Lock();
		    *done = true;
		    sig->Signal();
		    lock->Unlock();
		}
	    };
	    
	    cid_t* cid = new cid_t(&lock, &sig, &connmsg, &pmsg, &done);
	    
	    if (!lc.Init(sf::IPAddress(std::string(gopts.link_host.mb_str())), cid)) {
			wxLogError(_("Error occurred.\nPlease try again."));
			lock.Unlock();
			delete cid;
			return;
	    }
	    
	    wxProgressDialog
		pdlg(_("Waiting for connection..."), connmsg,
		     100, dlg, wxPD_APP_MODAL|wxPD_CAN_ABORT|wxPD_ELAPSED_TIME);
		     
	    while(!done) {
			if(!pdlg.Pulse(connmsg + pmsg))
				done = true;
			sig.Wait();
	    }
	}
	lock.Unlock();
	if(lanlink.connected) {
	    pmsg.Replace(wxT("\n"), wxT(" "));
	    systemScreenMessage(pmsg);
	    lanlink.active = true;
	    ev.Skip(); // all OK
	}
    }
} net_link_handler;
#endif

// manage the cheat list dialog
static class CheatList_t : public wxEvtHandler
{
public:
    wxDialog *dlg;
    wxCheckedListCtrl *list;
    wxListItem item0, item1;
    int col1minw;
    wxString cheatdir, cheatfn, deffn;
    bool isgb;
    bool *dirty;

    // add/edit dialog
    wxString ce_desc;
    wxString ce_codes;
    wxChoice *ce_type_ch;
    wxControl *ce_codes_tc;
    int ce_type;

    void Reload()
    {
	list->DeleteAllItems();
	Reload(0);
    }

    void Reload(int start)
    {
	if(isgb) {
	    for(int i = start; i < gbCheatNumber; i++) {
		item0.SetId(i);
		item0.SetText(wxString(gbCheatList[i].cheatCode, wxConvLibc));
		list->InsertItem(item0);
		item1.SetId(i);
		item1.SetText(wxString(gbCheatList[i].cheatDesc, wxConvUTF8));
		list->SetItem(item1);
		list->Check(i, gbCheatList[i].enabled);
	    }
	} else {
	    for(int i = start; i < cheatsNumber; i++) {
		item0.SetId(i);
		item0.SetText(wxString(cheatsList[i].codestring, wxConvLibc));
		list->InsertItem(item0);
		item1.SetId(i);
		item1.SetText(wxString(cheatsList[i].desc, wxConvUTF8));
		list->SetItem(item1);
		list->Check(i, cheatsList[i].enabled);
	    }
	}
	AdjustDescWidth();
    }

    void Tool(wxCommandEvent &ev)
    {
	switch(ev.GetId()) {
	case wxID_OPEN:
	    {
		wxFileDialog subdlg(dlg, _("Select cheat file"), cheatdir,
				    cheatfn, _("VBA cheat lists (*.clt)|*.clt"),
				    wxFD_OPEN|wxFD_FILE_MUST_EXIST);
		int ret = subdlg.ShowModal();
		cheatdir = subdlg.GetDirectory();
		cheatfn = subdlg.GetPath();
		if(ret != wxID_OK)
		    break;
		bool cld;
		if(isgb)
		    cld = gbCheatsLoadCheatList(cheatfn.mb_fn_str());
		else
		    cld = cheatsLoadCheatList(cheatfn.mb_fn_str());
		if(cld) {
		    *dirty = cheatfn != deffn;
		    systemScreenMessage(_("Loaded cheats"));
		} else
		    *dirty = true; // attempted load always clears
		Reload();
	    }
	    break;
	case wxID_SAVE:
	    {
		wxFileDialog subdlg(dlg, _("Select cheat file"), cheatdir,
				    cheatfn, _("VBA cheat lists (*.clt)|*.clt"),
				    wxFD_SAVE|wxFD_OVERWRITE_PROMPT);
		int ret = subdlg.ShowModal();
		cheatdir = subdlg.GetDirectory();
		cheatfn = subdlg.GetPath();
		if(ret != wxID_OK)
		    break;
		// note that there is no way to test for succes of save
		if(isgb)
		    gbCheatsSaveCheatList(cheatfn.mb_fn_str());
		else
		    cheatsSaveCheatList(cheatfn.mb_fn_str());
		if(cheatfn == deffn)
		    *dirty = false;
		systemScreenMessage(_("Saved cheats"));
	    }
	    break;
	case wxID_ADD:
	    {
		int ncheats = isgb ? gbCheatNumber : cheatsNumber;
		ce_codes = wxEmptyString;
		wxDialog *subdlg = GetXRCDialog("CheatEdit");
		subdlg->ShowModal();
		AddCheat();
		Reload(ncheats);
	    }
	    break;
	case wxID_REMOVE:
	    {
		bool asked = false, restore;
		for(int i = list->GetItemCount() - 1; i >= 0; i--)
		    if(list->GetItemState(i, wxLIST_STATE_SELECTED)) {
			list->DeleteItem(i);
			if(isgb)
			    gbCheatRemove(i);
			else {
			    if(!asked) {
				asked = true;
				restore = wxMessageBox(_("Restore old values?"),
						       _("Removing cheats"),
						       wxYES_NO|wxICON_QUESTION) == wxYES;
			    }
			    cheatsDelete(i, restore);
			}
		    }
	    }
	    break;
	case wxID_CLEAR:
	    if(isgb) {
		if(gbCheatNumber) {
		    *dirty = true;
		    gbCheatRemoveAll();
		}
	    } else {
		if(cheatsNumber) {
		    bool restore = wxMessageBox(_("Restore old values?"),
						_("Removing cheats"),
						wxYES_NO|wxICON_QUESTION) == wxYES;
		    *dirty = true;
		    cheatsDeleteAll(restore);
		}
	    }
	    Reload();
	    break;
	case wxID_SELECTALL:
	    // FIXME: probably ought to limit to selected items if any items
	    // are selected
	    *dirty = true;
	    if(isgb) {
		int i;
		for(i = 0; i < gbCheatNumber; i++)
		    if(!gbCheatList[i].enabled)
			break;
		if(i < gbCheatNumber)
		    for(; i < gbCheatNumber; i++) {
			gbCheatEnable(i);
			list->Check(i, true);
		    }
		else
		    for(i = 0; i < gbCheatNumber; i++) {
			gbCheatDisable(i);
			list->Check(i, false);
		    }
	    } else {
		int i;
		for(i = 0; i < cheatsNumber; i++)
		    if(!cheatsList[i].enabled)
			break;
		if(i < cheatsNumber)
		    for(; i < cheatsNumber; i++) {
			cheatsEnable(i);
			list->Check(i, true);
		    }
		else
		    for(i = 0; i < cheatsNumber; i++) {
			cheatsDisable(i);
			list->Check(i, false);
		    }
	    }
	    break;
	}
    }

    void Check(wxListEvent &ev)
    {
	int ch = ev.GetIndex();
	if(isgb) {
	    if(!gbCheatList[ch].enabled) {
		gbCheatEnable(ev.GetIndex());
		*dirty = true;
	    }
	} else {
	    if(!cheatsList[ch].enabled) {
		cheatsEnable(ev.GetIndex());
		*dirty = true;
	    }
	}
    }

    void UnCheck(wxListEvent &ev)
    {
	int ch = ev.GetIndex();
	if(isgb) {
	    if(gbCheatList[ch].enabled) {
		gbCheatDisable(ev.GetIndex());
		*dirty = true;
	    }
	} else {
	    if(cheatsList[ch].enabled) {
		cheatsDisable(ev.GetIndex());
		*dirty = true;
	    }
	}
    }

    void AddCheat()
    {
	wxStringTokenizer tk(ce_codes.MakeUpper());
	while(tk.HasMoreTokens()) {
	    wxString tok = tk.GetNextToken();
	    if(isgb) {
		if(!ce_type)
		    gbAddGsCheat(tok.mb_str(), ce_desc.mb_str());
		else
		    gbAddGgCheat(tok.mb_str(), ce_desc.mb_str());
	    } else {
		if(!ce_type)
		    cheatsAddCheatCode(tok.mb_str(), ce_desc.mb_str());
		// following determination of type by lengths is
		// same used by win32 and gtk code
		// and like win32/gtk code, user-chosen fmt is ignored
		else if(tok.size() == 12) {
		    tok = tok.substr(0, 8) + wxT(' ') + tok.substr(8);
		    cheatsAddCBACode(tok.mb_str(), ce_desc.mb_str());
		} else if(tok.size() == 16)
			// not sure why 1-tok is !v3 and 2-tok is v3..
			cheatsAddGSACode(tok.mb_str(), ce_desc.mb_str(), false);
		// CBA codes are assumed to be N+4, and anything else
		// is assumed to be GSA v3 (although I assume the
		// actual formats should be 8+4 and 8+8)
		else {
		    if(!tk.HasMoreTokens()) {
			// throw an error appropriate to chosen type
			if(ce_type == 1) // GSA
			    cheatsAddGSACode(tok.mb_str(), ce_desc.mb_str(), false);
			else
			    cheatsAddCBACode(tok.mb_str(), ce_desc.mb_str());
		    } else {
			wxString tok2 = tk.GetNextToken();
			if(tok2.size() == 4) {
			    tok += wxT(' ') + tok2;
			    cheatsAddCBACode(tok.mb_str(), ce_desc.mb_str());
			} else {
			    tok += tok2;
			    cheatsAddGSACode(tok.mb_str(), ce_desc.mb_str(), true);
			}
		    }
		}
	    }
	}
    }

    void Edit(wxListEvent &ev)
    {
	int id = ev.GetIndex();

	// GetItem() followed by GetText doesn't work, so retrieve from
	// source
	wxString odesc, ocode;
	bool ochecked;
	int otype;
	if(isgb) {
	    ochecked = gbCheatList[id].enabled;
	    ce_codes = ocode = wxString(gbCheatList[id].cheatCode, wxConvLibc);
	    ce_desc = odesc = wxString(gbCheatList[id].cheatDesc, wxConvUTF8);
	    if(ce_codes.find(wxT('-')) == wxString::npos)
		otype = ce_type = 0;
	    else
		otype = ce_type = 1;
	} else {
	    ochecked = cheatsList[id].enabled;
	    ce_codes = ocode = wxString(cheatsList[id].codestring, wxConvLibc);
	    ce_desc = odesc = wxString(cheatsList[id].desc, wxConvUTF8);
	    if(ce_codes.find(wxT(':')) != wxString::npos)
		otype = ce_type = 0;
	    else if(ce_codes.find(wxT(' ')) == wxString::npos)
		otype = ce_type = 1;
	    else
		otype = ce_type = 2;
	}
	wxDialog *subdlg = GetXRCDialog("CheatEdit");
	if(subdlg->ShowModal() != wxID_OK)
	    return;
	if(otype != ce_type || ocode != ce_codes) {
	    // vba core certainly doesn't make this easy
	    // there is no "change" function, so the only way to retain
	    // the old order is to delete this and all subsequent items, and
	    // then re-add them
	    // The MFC code got around this by not even supporting edits on
	    // gba codes (which have order dependencies) and just forcing
	    // edited codes to the rear on gb codes.
	    // It might be safest to only support desc edits, and force the
	    // user to re-enter codes to change them
	    int ncodes = isgb ? gbCheatNumber : cheatsNumber;
	    if(ncodes > id + 1) {
		wxString codes[ncodes - id - 1];
		wxString descs[ncodes - id - 1];
		bool checked[ncodes - id - 1];
		bool v3[ncodes - id - 1];
		for(int i = id + 1; i < ncodes; i++) {
		    codes[i - id - 1] = wxString(isgb ?
						 gbCheatList[i].cheatCode :
						 cheatsList[i].codestring,
						 wxConvLibc);
		    descs[i - id - 1] = wxString(isgb ?
						 gbCheatList[i].cheatDesc :
						 cheatsList[i].desc,
						 wxConvUTF8);
		    checked[i - id - 1] = isgb ? gbCheatList[i].enabled :
			                         cheatsList[i].enabled;
		    v3[i - id - 1] = isgb ? false : cheatsList[i].code == 257;
		}
		for(int i = ncodes - 1; i >= id; i--) {
		    list->DeleteItem(i);
		    if(isgb)
			gbCheatRemove(i);
		    else
			cheatsDelete(i, cheatsList[i].enabled);
		}
		AddCheat();
		if(!ochecked) {
		    if(isgb)
			gbCheatDisable(id);
		    else
			cheatsDisable(id);
		}
		for(int i = id + 1; i < ncodes; i++) {
		    ce_codes = codes[i - id - 1];
		    ce_desc = descs[i - id - 1];
		    if(isgb) {
			if(ce_codes.find(wxT('-')) == wxString::npos)
			    ce_type = 0;
			else
			    ce_type = 1;
		    } else {
			if(ce_codes.find(wxT(':')) != wxString::npos)
			    ce_type = 0;
			else if(ce_codes.find(wxT(' ')) == wxString::npos) {
			    ce_type = 1;
			    if(v3[i - id - 1])
				ce_codes.insert(8, 1, wxT(' '));
			} else
			    ce_type = 2;
		    }
		    AddCheat();
		    if(!checked[i - id - 1]) {
			if(isgb)
			    gbCheatDisable(i);
			else
			    cheatsDisable(i);
		    }
		}
	    } else {
		list->DeleteItem(id);
		if(isgb)
		    gbCheatRemove(id);
		else
		    cheatsDelete(id, cheatsList[id].enabled);
		AddCheat();
		if(!ochecked) {
		    if(isgb)
			gbCheatDisable(id);
		    else
			cheatsDisable(id);
		}
	    }
	    Reload(id);
	} else if(ce_desc != odesc) {
	    *dirty = true;
	    char *p = isgb ? gbCheatList[id].cheatDesc : cheatsList[id].desc;
	    strncpy(p, ce_desc.mb_str(), sizeof(cheatsList[0].desc));
	    p[sizeof(cheatsList[0].desc) - 1] = 0;
	    item1.SetId(id);
	    item1.SetText(wxString(p, wxConvUTF8));
	    list->SetItem(item1);
	}
    }

    void AdjustDescWidth()
    {
	// why is it so hard to get an accurate measurement out of wx?
	// on msw, wxLIST_AUTOSIZE might actually be accurate.  On wxGTK,
	// and probably wxMAC (both of which use generic impl) wrong
	// font is used both for rendering (col 0's font) and for
	// wxLIST_AUTOSIZE calculation (the widget's font).
	// The only way to defeat this is to calculate size manually
	// Instead, this just allows user to set max size, and retains
	// it.
	int ow = list->GetColumnWidth(1);
	list->SetColumnWidth(1, wxLIST_AUTOSIZE);
	int cw = list->GetColumnWidth(1);
	// subtracted in renderer from width avail for text
	// but not added in wxLIST_AUTOSIZE
	cw += 8;
	if(cw < col1minw)
	    cw = col1minw;
	if(cw < ow)
	    cw = ow;
	list->SetColumnWidth(1, cw);
    }
} cheat_list_handler;

// onshow handler for above, in the form of an overzealous validator
class CheatListFill : public wxValidator
{
public:
    CheatListFill() : wxValidator() {}
    CheatListFill(const CheatListFill &e) : wxValidator() {}
    wxObject *Clone() const { return new CheatListFill(*this); }
    bool TransferFromWindow() { return true; }
    bool Validate(wxWindow *p) { return true; }
    bool TransferToWindow() {
	CheatList_t &clh = cheat_list_handler;
	GameArea *panel = wxGetApp().frame->GetPanel();
	clh.isgb = panel->game_type() == IMAGE_GB;
	clh.dirty = &panel->cheats_dirty;
	clh.cheatfn = panel->game_name() + wxT(".clt");
	clh.cheatdir = panel->game_dir();
	clh.deffn = wxFileName(clh.cheatdir, clh.cheatfn).GetFullPath();
	clh.Reload();

	clh.ce_desc = wxEmptyString;
	wxChoice *ch = clh.ce_type_ch;
	ch->Clear();
	if(clh.isgb) {
	    ch->Append(_("GameShark"));
	    ch->Append(_("GameGenie"));
	} else {
	    ch->Append(_("Generic Code"));
	    ch->Append(_("GameShark Advance"));
	    ch->Append(_("CodeBreaker Advance"));
	}
	ch->SetSelection(0);

	return true;
    }
};

// manage the cheat search dialog
enum cf_vfmt {
    CFVFMT_SD, CFVFMT_UD, CFVFMT_UH
};

// virtual ListCtrl for cheat search results
class CheatListCtrl : public wxListCtrl
{
  public:
    wxArrayInt addrs;
    int cap_size; // size in effect when addrs were generated
    int count8, count16, count32; // number of aligned addresses in addrs
    wxString OnGetItemText(long item, long column) const;

    DECLARE_DYNAMIC_CLASS()
};

IMPLEMENT_DYNAMIC_CLASS(CheatListCtrl, wxListCtrl);

static class CheatFind_t : public wxEvtHandler
{
public:
    wxDialog *dlg;
    int valsrc, size, op, fmt;
    int ofmt, osize;
    wxString val_s;
    wxTextCtrl *val_tc;
    CheatListCtrl *list;

    // for enable/disable
    wxRadioButton *old_rb, *val_rb;
    wxControl *update_b, *clear_b, *add_b;

    bool isgb;

    // add dialog
    wxString ca_desc, ca_val;
    wxTextCtrl *ca_val_tc;
    wxControl *ca_fmt, *ca_addr;

    CheatFind_t() : wxEvtHandler(), valsrc(0), size(0), op(0), fmt(0), val_s() {}
    ~CheatFind_t()
    {
	// not that it matters to anyone but mem leak detectors..
	cheatSearchCleanup(&cheatSearchData);
    }

    void Search(wxCommandEvent &ev)
    {
	dlg->TransferDataFromWindow();
	if(!valsrc && val_s.empty()) {
	    wxLogError(_("Number cannot be empty"));
	    return;
	}
	if(!cheatSearchData.count)
	    ResetSearch(ev);
	if(valsrc)
	    cheatSearch(&cheatSearchData, op, size, fmt == CFVFMT_SD);
	else
	    cheatSearchValue(&cheatSearchData, op, size, fmt == CFVFMT_SD,
			     SignedValue());
	Deselect();
	list->addrs.clear();
	list->count8 = list->count16 = list->count32 = 0;
	list->cap_size = size;
	for(int i = 0; i < cheatSearchData.count; i++) {
	    CheatSearchBlock *block = &cheatSearchData.blocks[i];
	    for(int j = 0; j < block->size; j += (1 << size)) {
		if(IS_BIT_SET(block->bits, j)) {
		    list->addrs.push_back((i << 28) + j);
		    if(!(j & 1))
			list->count16++;
		    if(!(j & 3))
			list->count32++;
		    // since listctrl is virtual, it should be able to handle
		    // at least 256k results, which is about the most you
		    // will ever get
#if 0
		    if(list->addrs.size() > 1000) {
			wxLogError(_("Search produced %d results.  Please refine better"),
				   list->addrs.size());
			list->addrs.clear();
			return;
		    }
#endif
		}
	    }
	}
	if(list->addrs.empty()) {
	    wxLogError(_("Search produced no results"));
	    // no point in keeping empty search results around
	    ResetSearch(ev);
	    if(old_rb->GetValue()) {
		val_rb->SetValue(true);
		// SetValue doesn't generate an event
		val_tc->Enable();
	    }
	    old_rb->Disable();
	    update_b->Disable();
	    clear_b->Disable();
	} else {
	    switch(size) {
	      case BITS_32:
		list->count16 = list->count32 * 2;
		// fall through
	      case BITS_16:
		list->count8 = list->count16 * 2;
		break;
	      case BITS_8:
		list->count8 = list->addrs.size();
	    }
	    old_rb->Enable();
	    update_b->Enable();
	    clear_b->Enable();
	}
	list->SetItemCount(list->addrs.size());
	list->Refresh();
    }

    void UpdateVals(wxCommandEvent &ev)
    {
	if(cheatSearchData.count) {
	    cheatSearchUpdateValues(&cheatSearchData);
	    if(list->count8)
		list->Refresh();
	    update_b->Disable();
	}
    }

    void ResetSearch(wxCommandEvent &ev)
    {
	if(!cheatSearchData.count) {
	    CheatSearchBlock *block = cheatSearchData.blocks;

	    if(isgb) {
		block->offset = 0xa000;
		if(gbRam)
		    block->data = gbRam;
		else
		    block->data = &gbMemory[0xa000];
		block->saved = (u8 *)malloc(gbRamSize);
		block->size = gbRamSize;
		block->bits = (u8 *)malloc(gbRamSize >> 3);
		if(gbCgbMode) {
		    block++;
		    block->offset = 0xc000;
		    block->data = &gbMemory[0xc000];
		    block->saved = (u8 *)malloc(0x1000);
		    block->size = 0x1000;
		    block->bits = (u8 *)malloc(0x1000 >> 3);
		    block++;
		    block->offset = 0xd000;
		    block->data = gbWram;
		    block->saved = (u8 *)malloc(0x8000);
		    block->size = 0x8000;
		    block->bits = (u8 *)malloc(0x8000 >> 3);
		} else {
		    block++;
		    block->offset = 0xc000;
		    block->data = &gbMemory[0xc000];
		    block->saved = (u8 *)malloc(0x2000);
		    block->size = 0x2000;
		    block->bits = (u8 *)malloc(0x2000 >> 3);
		}
	    } else {
		block->size = 0x40000;
		block->offset = 0x2000000;
		block->bits = (u8 *)malloc(0x40000 >> 3);
		block->data = workRAM;
		block->saved = (u8 *)malloc(0x40000);
		block++;
		block->size = 0x8000;
		block->offset = 0x3000000;
		block->bits = (u8 *)malloc(0x8000 >> 3);
		block->data = internalRAM;
		block->saved = (u8 *)malloc(0x8000);
	    }
	    cheatSearchData.count = (int)((block + 1) - cheatSearchData.blocks);
	}
	cheatSearchStart(&cheatSearchData);
	if(list->count8) {
	    Deselect();
	    list->count8 = list->count16 = list->count32 = 0;
	    list->addrs.clear();
	    list->SetItemCount(0);
	    list->Refresh();
	    if(old_rb->GetValue()) {
		val_rb->SetValue(true);
		// SetValue doesn't generate an event
		val_tc->Enable();
	    }
	    old_rb->Disable();
	    update_b->Disable();
	    clear_b->Disable();
	}
    }

    void Deselect()
    {
	int idx = list->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
	if(idx >= 0)
	    list->SetItemState(idx, 0, wxLIST_STATE_SELECTED);
	add_b->Disable();
    }

    void Select(wxListEvent &ev)
    {
	add_b->Enable(list->GetItemState(ev.GetIndex(), wxLIST_STATE_SELECTED) != 0);
    }

    void AddCheatB(wxCommandEvent &ev)
    {
	int idx = list->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
	if(idx >= 0)
	    AddCheat(idx);
    }

    void AddCheatL(wxListEvent &ev)
    {
	AddCheat(ev.GetIndex());
    }

    void AddCheat(int idx)
    {
	wxString addr_s = list->OnGetItemText(idx, 0);
	ca_addr->SetLabel(addr_s);
	wxString s;
	switch(size) {
	  case BITS_8:
	    s = _("8-bit ");
	    break;
	  case BITS_16:
	    s = _("16-bit ");
	    break;
	  case BITS_32:
	    s = _("32-bit ");
	    break;
	}
	switch(fmt) {
	  case CFVFMT_SD:
	    s += _("signed decimal");
	    break;
	  case CFVFMT_UD:
	    s += _("unsigned decimal");
	    break;
	  case CFVFMT_UH:
	    s += _("unsigned hexadecimal");
	    break;
	}
	ca_fmt->SetLabel(s);
	// probably pointless (but inoffensive) to suggest a value
	ca_val = list->OnGetItemText(idx, 2); // sugest "New" value
	SetValVal(ca_val_tc);
	wxDialog *subdlg = GetXRCDialog("CheatAdd");
	if(subdlg->ShowModal() != wxID_OK)
	    return;
	if(ca_val.empty()) {
	    wxLogError(_("Number cannot be empty"));
	    return;
	}
	u32 val = GetValue(ca_val, fmt);
	if(isgb) {
	    long bank, addr;
	    addr_s.ToLong(&bank, 16);
	    addr_s.erase(0, 3);
	    addr_s.ToLong(&addr, 16);
	    if(addr >= 0xd000)
		bank += 0x90;
	    else
		bank = 1;
	    for(int i = 0; i < (1 << size); i++) {
		addr_s.Printf(wxT("%02X%02X%02X%02X"), bank, val & 0xff,
			      addr & 0xff, addr >> 8);
		gbAddGsCheat(addr_s.mb_str(), ca_desc.mb_str());
		val >>= 8;
		addr++;
	    }
	} else {
	    wxString s;
	    switch(size) {
	      case BITS_8:
		s.Printf(wxT(":%02X"), val);
		break;
	      case BITS_16:
		s.Printf(wxT(":%04X"), val);
		break;
	      case BITS_32:
		s.Printf(wxT(":%08X"), val);
		break;
	    }
	    addr_s.append(s);
	    cheatsAddCheatCode(addr_s.mb_str(), ca_desc.mb_str());
	}
    }

    void SetValVal(wxTextCtrl *tc)
    {
	wxTextValidator *v = wxStaticCast(tc->GetValidator(), wxTextValidator);
	switch(fmt) {
	  case CFVFMT_SD:
	    v->SetIncludes(val_sigdigits);
	    break;
	  case CFVFMT_UD:
	    v->SetIncludes(val_unsdigits);
	    break;
	  case CFVFMT_UH:
	    v->SetIncludes(val_hexdigits);
	    break;
	}
    }

    u32 GetValue(wxString &s, int fmt)
    {
	long val;
	// FIXME: probably ought to throw an error if ToLong
	// returns false or val is out of range
	s.ToLong(&val, fmt == CFVFMT_UH ? 16 : 10);
	if(size != BITS_32)
	    val &= size == BITS_8 ? 0xff : 0xffff;
	return val;
    }

    u32 GetValue(int fmt)
    {
	return GetValue(val_s, fmt);
    }

    u32 GetValue()
    {
	return GetValue(fmt);
    }

    s32 SignedValue(wxString &s, int fmt)
    {
	s32 val = GetValue(s, fmt);
	if(fmt == CFVFMT_SD) {
	    if(size == BITS_8)
		val = (s32)(s8)val;
	    else if(size == BITS_16)
		val = (s32)(s16)val;
	}
	return val;
    }

    s32 SignedValue(int fmt)
    {
	return SignedValue(val_s, fmt);
    }

    s32 SignedValue()
    {
	return SignedValue(fmt);
    }

    void FormatValue(s32 val, wxString &s)
    {
	if(fmt != CFVFMT_SD && size != BITS_32)
	    val &= size == BITS_8 ? 0xff : 0xffff;
	switch(fmt) {
	  case CFVFMT_SD:
	    s.Printf(wxT("%d"), val);
	    break;
	  case CFVFMT_UD:
	    s.Printf(wxT("%u"), val);
	    break;
	  case CFVFMT_UH:
	    switch(size) {
	      case BITS_8:
		s.Printf(wxT("%02X"), val);
		break;
	      case BITS_16:
		s.Printf(wxT("%04X"), val);
		break;
	      case BITS_32:
		s.Printf(wxT("%08X"), val);
		break;
	    }
	}
    }

    void UpdateView(wxCommandEvent &ev)
    {
	dlg->TransferDataFromWindow();
	if(ofmt != fmt && !val_s.empty()) {
	    s32 val = GetValue(ofmt);
	    switch(fmt) {
	      case CFVFMT_SD:
		switch(size) {
		  case BITS_8:
		    val = (s32)(s8)val;
		    break;
		  case BITS_16:
		    val = (s32)(s16)val;
		}
		val_s.Printf(wxT("%d"), val);
		break;
	      case CFVFMT_UD:
		val_s.Printf(wxT("%u"), val);
		break;
	      case CFVFMT_UH:
		val_s.Printf(wxT("%x"), val);
		break;
	    }
	    val_tc->SetValue(val_s);
	}
	if(ofmt != fmt)
	    SetValVal(val_tc);
	if(list->count8 && osize != size) {
	    switch(size) {
	      case BITS_32:
		list->SetItemCount(list->count32);
		break;
	      case BITS_16:
		list->SetItemCount(list->count16);
		break;
	      case BITS_8:
		list->SetItemCount(list->count8);
		break;
	    }
	}
	if(ofmt != fmt || osize != size)
	    list->Refresh();
	ofmt = fmt;
	osize = size;
    }

    void EnableVal(wxCommandEvent &ev)
    {
	val_tc->Enable(ev.GetId() == XRCID("SpecificValue"));
    }

} cheat_find_handler;

// clear cheat find dialog between games
void MainFrame::ResetCheatSearch()
{
    CheatFind_t &cfh = cheat_find_handler;
    cfh.fmt = cfh.size = cfh.op = cfh.valsrc = 0;
    cfh.val_s = wxEmptyString;
    cfh.Deselect();
    cfh.list->SetItemCount(0);
    cfh.list->count8 = cfh.list->count16 = cfh.list->count32 = 0;
    cfh.list->addrs.clear();
    cfh.ca_desc = wxEmptyString;
    cheatSearchCleanup(&cheatSearchData);
}

// onshow handler for above, in the form of an overzealous validator
class CheatFindFill : public wxValidator
{
public:
    CheatFindFill() : wxValidator() {}
    CheatFindFill(const CheatFindFill &e) : wxValidator() {}
    wxObject *Clone() const { return new CheatFindFill(*this); }
    bool TransferFromWindow() { return true; }
    bool Validate(wxWindow *p) { return true; }
    bool TransferToWindow() {
	CheatFind_t &cfh = cheat_find_handler;
	GameArea *panel = wxGetApp().frame->GetPanel();
	cfh.isgb = panel->game_type() == IMAGE_GB;
	cfh.val_tc->Enable(!cfh.valsrc);
	cfh.ofmt = cfh.fmt;
	cfh.SetValVal(cfh.val_tc);
	return true;
    }
};

// the implementation of the virtual list ctrl for search results
// requires CheatFind_t to be implemented
wxString CheatListCtrl::OnGetItemText(long item, long column) const
{
    wxString s;
    CheatFind_t &cfh = cheat_find_handler;
    // allowing GUI to change format after search makes this a little
    // more complicated than necessary...
    int off = 0;
    int size = cfh.size;
    if(cap_size > size) {
	off = (item & ((1 << (cap_size - size)) - 1)) << size;
	item >>= cap_size - size;
    } else if(cap_size < size) {
	for(int i = 0; i < addrs.size(); i++) {
	    if(!(addrs[i] & ((1 << size) - 1)) && !item--) {
		item = i;
		break;
	    }
	}
    }
    CheatSearchBlock *block = &cheatSearchData.blocks[addrs[item] >> 28];
    off += addrs[item] & 0xfffffff;

    switch(column) {
      case 0: // address
	if(cfh.isgb) {
	    int bank = 0;
	    int addr = block->offset;
	    if(block->offset == 0xa000) {
		bank = off / 0x2000;
		addr += off % 0x2000;
	    } else if(block->offset == 0xd000) {
		bank = off / 0x1000;
		addr += off % 0x1000;
	    } else
		addr += off;
	    s.Printf(wxT("%02X:%04X"), bank, addr);
	} else
	    s.Printf(wxT("%08X"), block->offset + off);
	break;
      case 1: // old
	cfh.FormatValue(cheatSearchSignedRead(block->saved, off, size), s);
	break;
      case 2: // new
	cfh.FormatValue(cheatSearchSignedRead(block->data, off, size), s);
	break;
    }
    return s;
}

// these are the choices for canned colors; their order must match the
// names in the choice control
static const u16 defaultPalettes[][8] = {
  { // Standard
    0x7FFF, 0x56B5, 0x318C, 0x0000,  0x7FFF, 0x56B5, 0x318C, 0x0000,
  },
  { // Blue Sea
    0x6200, 0x7E10, 0x7C10, 0x5000,  0x6200, 0x7E10, 0x7C10, 0x5000,
  },
  { // Dark Night
    0x4008, 0x4000, 0x2000, 0x2008,  0x4008, 0x4000, 0x2000, 0x2008,
  },
  { // Green Forest
    0x43F0, 0x03E0, 0x4200, 0x2200,  0x43F0, 0x03E0, 0x4200, 0x2200,
  },
  { // Hot Desert
    0x43FF, 0x03FF, 0x221F, 0x021F,  0x43FF, 0x03FF, 0x221F, 0x021F,
  },
  { // Pink Dreams
    0x621F, 0x7E1F, 0x7C1F, 0x2010,  0x621F, 0x7E1F, 0x7C1F, 0x2010,
  },
  { // Weird Colors
    0x621F, 0x401F, 0x001F, 0x2010,  0x621F, 0x401F, 0x001F, 0x2010,
  },
  { // Real GB Colors
    0x1B8E, 0x02C0, 0x0DA0, 0x1140,  0x1B8E, 0x02C0, 0x0DA0, 0x1140,
  },
  { // Real 'GB on GBASP' Colors
    0x7BDE, /*0x23F0*/ 0x5778, /*0x5DC0*/ 0x5640, 0x0000,  0x7BDE, /*0x3678*/ 0x529C, /*0x0980*/ 0x2990, 0x0000,
  }
};

// manage the GB color prefs' canned color selecter
static class GBColorConfig_t : public wxEvtHandler
{
public:
    wxWindow *p;
    wxChoice *c;
    wxColourPickerCtrl *cp[8];
    int pno;
    void ColorSel(wxCommandEvent &ev)
    {
	if(ev.GetSelection() > 0) {
	    const u16 *color = defaultPalettes[ev.GetSelection() - 1];
	    for(int i = 0; i < 8; i++, color++)
		cp[i]->SetColour(wxColor(((*color << 3) & 0xf8),
					 ((*color >> 2) & 0xf8),
					 ((*color >> 7) & 0xf8)));
	}
    }
    void ColorReset(wxCommandEvent &ev)
    {
	const u16 *color = &systemGbPalette[pno * 8];
	for(int i = 0; i < 8; i++, color++)
	    cp[i]->SetColour(wxColor(((*color << 3) & 0xf8),
				     ((*color >> 2) & 0xf8),
				     ((*color >> 7) & 0xf8)));
    }

    void ColorButton(wxCommandEvent &ev)
    {
	c->SetSelection(0);
    }
} GBColorConfigHandler[3];

// disable controls if a GBA game is not loaded
class GBACtrlEnabler : public wxValidator
{
public:
    GBACtrlEnabler() : wxValidator() {}
    GBACtrlEnabler(const GBACtrlEnabler &e) : wxValidator() {}
    wxObject *Clone() const { return new GBACtrlEnabler(*this); }
    bool TransferFromWindow() { return true; }
    bool Validate(wxWindow *p) { return true; }
    bool TransferToWindow() {
	GetWindow()->Enable(wxGetApp().frame->GetPanel()->game_type() == IMAGE_GBA);
	return true;
    }
};

// manage save game area settings for GBA prefs
static class BatConfig_t : public wxEvtHandler
{
public:
    wxChoice *type, *size;
    void ChangeType(wxCommandEvent &ev)
    {
	int i = ev.GetSelection();
	size->Enable(!i || i == 3); // automatic/flash
    }
    void Detect(wxCommandEvent &ev)
    {
	// note: win32 version just pops up a dialog stating what it found
	// which is appropriate becauase it was in a menu
	// this code sets the controls instead, since there right there
	u32 sz = wxGetApp().frame->GetPanel()->game_size();
#define ch4(a, b, c, d) \
    wxUINT32_SWAP_ON_BE(a + (b << 8) + (c << 16) + (d << 24))
	for(u32 addr = 0; addr < sz - 10; addr += 4) {
	    switch(*(u32 *)&rom[addr]) {
	    case ch4('E', 'E', 'P', 'R'):
		if(memcmp(&rom[addr + 4], "OM_V", 4))
		    break;
		// apparently no sensor autodetection
		type->SetSelection(1);
		size->Disable();
		return;
	    case ch4('S', 'R', 'A', 'M'):
		if(memcmp(&rom[addr + 4], "_V", 2))
		    break;
		type->SetSelection(2);
		size->Disable();
		return;
	    case ch4('F', 'L', 'A', 'S'):
		if(!memcmp(&rom[addr + 4], "H_V", 3)) {
		    type->SetSelection(3);
		    size->SetSelection(0);
		    size->Enable();
		    return;
		} else if(!memcmp(&rom[addr + 4], "H1M_V", 5)) {
		    type->SetSelection(3);
		    size->SetSelection(1);
		    size->Enable();
		    return;
		}
		break;
	    }
	}
	type->SetSelection(5);
	size->Disable();
    }
} BatConfigHandler;

// manage the sound prefs dialog
static class SoundConfig_t : public wxEvtHandler
{
public:
    wxSlider *vol, *bufs;
    wxControl *bufinfo;
    int lastapi;
    wxChoice *dev;
    wxControl *umix, *hwacc;
    wxArrayString dev_ids;

    void FullVol(wxCommandEvent &ev)
    {
	vol->SetValue(100);
    }
    void AdjustFrames(int count)
    {
	wxString s;
	s.Printf(_("%d frames = %.2f ms"), count, (double)count / 60.0 * 1000.0);
	bufinfo->SetLabel(s);
    }
    void AdjustFramesEv(wxCommandEvent &ev)
    {
	AdjustFrames(bufs->GetValue());
    }

    bool FillDev(int api)
    {
	dev->Clear();
	dev->Append(_("Default device"));
	dev_ids.clear();
	wxArrayString names;
	switch(api) {
	case AUD_SDL:
	    break;
#ifndef NO_OAL
	case AUD_OPENAL:
	    if(!GetOALDevices(names, dev_ids))
		return false;
	    break;
#endif
#ifdef __WXMSW__
	case AUD_DIRECTSOUND:
	    if(!(GetDSDevices(names, dev_ids)))
		return false;
	    break;
#ifndef NO_XAUDIO2
	case AUD_XAUDIO2:
	    if(!GetXA2Devices(names, dev_ids))
		return false;
	    break;
#endif
#endif
	}
	dev->SetSelection(0);
	for(int i = 0; i < names.size(); i++) {
	    dev->Append(names[i]);
	    if(api == gopts.audio_api && gopts.audio_dev == dev_ids[i])
		dev->SetSelection(i + 1);
	}
	umix->Enable(api == AUD_XAUDIO2);
	hwacc->Enable(api == AUD_DIRECTSOUND);
	lastapi = api;
    }
    void SetAPI(wxCommandEvent &ev)
    {
	int api = gopts.audio_api;
	wxValidator *v = wxStaticCast(ev.GetEventObject(), wxWindow)->GetValidator();
	v->TransferFromWindow();
	int newapi = gopts.audio_api;
	gopts.audio_api = api;
	if(newapi == lastapi)
	    return;
	FillDev(newapi);
    }
} sound_config_handler;

// Validator/widget filler for sound device selector & time indicator
class SoundConfigLoad : public wxValidator
{
public:
    SoundConfigLoad() : wxValidator() {}
    SoundConfigLoad(const SoundConfigLoad &e) : wxValidator() {}
    wxObject *Clone() const { return new SoundConfigLoad(*this); }
    bool Validate(wxWindow *p) { return true; }
    bool TransferToWindow() {
	SoundConfig_t &sch = sound_config_handler;
	sch.FillDev(gopts.audio_api);
	sch.AdjustFrames(gopts.audio_buffers);
	return true;
    }
    bool TransferFromWindow() {
	SoundConfig_t &sch = sound_config_handler;
	int devs = sch.dev->GetSelection();
	if(!devs)
	    gopts.audio_dev = wxEmptyString;
	else
	    gopts.audio_dev = sch.dev_ids[devs - 1];
	return true;
    }
};

// manage the joypad prefs' per-panel default/clear buttons
static class JoyPadConfig_t : public wxEvtHandler
{
public:
    wxWindow *p;
    void JoypadConfigButtons(wxCommandEvent &ev) {
	bool clear = ev.GetId() == XRCID("Clear");
	for(int i = 0; i < NUM_KEYS; i++) {
	    wxJoyKeyTextCtrl *tc = XRCCTRL_D(*p, joynames[i], wxJoyKeyTextCtrl);
	    if(clear)
		tc->SetValue(wxEmptyString);
	    else {
		wxJoyKeyBinding_v a;
		if(defkeys[i*2].key)
		    a.push_back(defkeys[i*2]);
		if(defkeys[i*2+1].joy)
		    a.push_back(defkeys[i*2+1]);
		tc->SetValue(wxJoyKeyTextCtrl::ToString(a));
	    }
	}

    }
} JoyPadConfigHandler[4];

#ifndef NO_LINK
// tc validator for IP addresses using SFML for validation instead of wx
class IPHostValidator : public wxValidator
{
    wxString *valp;
public:
    IPHostValidator(wxString *v) : wxValidator(), valp(v) {}
    IPHostValidator(const IPHostValidator &e) : wxValidator(), valp(e.valp) {}
    wxObject *Clone() const { return new IPHostValidator(*this); }
    bool Validate(wxWindow *p) {
	wxTextCtrl *tc = wxStaticCast(GetWindow(), wxTextCtrl);
	if(!tc->IsEnabled())
	    return true;
	wxString val = tc->GetValue();
	bool isv = true;
	if(val.empty())
	    isv = false;
	else {
	    sf::IPAddress srv = std::string(val.mb_str());
	    isv = srv.IsValid();
	}
	if(!isv)
	    wxMessageBox(_("You must enter a valid host name"),
			 _("Host name invalid"), wxICON_ERROR|wxOK);
	return isv;
    }
    bool TransferToWindow() {
	wxTextCtrl *tc = wxStaticCast(GetWindow(), wxTextCtrl);
	tc->SetValue(*valp);
	return true;
    }
    bool TransferFromWindow() {
	wxTextCtrl *tc = wxStaticCast(GetWindow(), wxTextCtrl);
	*valp = tc->GetValue();
	return true;
    }
};
#endif

// manage fullscreen mode widget
// technically, it's more than a validator: it modifies the widget as well
class ScreenModeList : public wxValidator
{
public:
    ScreenModeList() : wxValidator() {}
    ScreenModeList(const ScreenModeList &e) : wxValidator() {}
    wxObject *Clone() const { return new ScreenModeList(*this); }
    bool Validate(wxWindow *p) { return true; }
    bool TransferToWindow()
    {
	wxChoice *c = wxStaticCast(GetWindow(), wxChoice);
	wxDisplay d(wxDisplay::GetFromWindow(c->GetParent()));
	c->Clear();
	int modeno = 0, bestmode = 0;
	int bm_bpp = 0;
	c->Append(_("Desktop mode"));
	// probably ought to just disable this whole control on UNIX/X11 since
	// wxDisplay is so broken.
	vm = d.GetModes();
	wxString s;
	for(int i = 0; i < vm.size(); i++) {
	    s.Printf(_("%d x %d - %dbpp @ %dHz"), vm[i].w, vm[i].h, vm[i].bpp, vm[i].refresh);
	    c->Append(s);
	    if(!modeno && gopts.fs_mode.w == vm[i].w && gopts.fs_mode.h == vm[i].h) {
		if(gopts.fs_mode.bpp == vm[i].bpp && gopts.fs_mode.refresh == vm[i].refresh)
		    modeno = i + 1;
		else if(vm[i].bpp == gopts.fs_mode.bpp &&
			bm_bpp != gopts.fs_mode.bpp) {
		    bestmode = i + 1;
		    bm_bpp = vm[i].bpp;
		} else if(bm_bpp != gopts.fs_mode.bpp && bm_bpp != 32 &&
			  vm[i].bpp == 32) {
		    bm_bpp = vm[i].bpp;
		    bestmode = i + 1;
		} else if(bm_bpp != gopts.fs_mode.bpp && bm_bpp < 24 &&
			  vm[i].bpp == 24) {
		    bm_bpp = vm[i].bpp;
		    bestmode = i + 1;
		} else if(bm_bpp != gopts.fs_mode.bpp && bm_bpp < 24 &&
			  bm_bpp != 16 && vm[i].bpp == 16) {
		    bm_bpp = vm[i].bpp;
		    bestmode = i + 1;
		} else if(!bm_bpp) {
		    bm_bpp = vm[i].bpp;
		    bestmode = i + 1;
		}
	    }
	}
	if(!modeno && bestmode)
	    modeno = bestmode;
	c->SetSelection(modeno);
	return true;
    }
    bool TransferFromWindow()
    {
	int bestmode = wxStaticCast(GetWindow(), wxChoice)->GetSelection();
	if(!bestmode)
	    gopts.fs_mode.h = gopts.fs_mode.w = gopts.fs_mode.bpp = gopts.fs_mode.refresh = 0;
	else
	    gopts.fs_mode = vm[bestmode - 1];
	return true;
    }
private:
    wxArrayVideoModes vm;
};

// enable plugin-related iff filter choice is plugin
class PluginEnabler : public wxValidator
{
public:
    PluginEnabler() : wxValidator() {}
    PluginEnabler(const PluginEnabler &e) : wxValidator() {}
    wxObject *Clone() const { return new PluginEnabler(*this); }
    bool TransferFromWindow() { return true; }
    bool Validate(wxWindow *p) { return true; }
    bool TransferToWindow()
    {
	GetWindow()->Enable(gopts.filter == FF_PLUGIN);
	return true;
    }
};

// The same, but as an event handler
static class PluginEnable_t : public wxEvtHandler
{
public:
    wxWindow *lab, *ch;
    void ToggleChoice(wxCommandEvent &ev)
    {
	bool en = ev.GetSelection() == FF_PLUGIN;
	lab->Enable(en);
	ch->Enable(en);
    }
} PluginEnableHandler;

// fill in plugin list
class PluginListFiller : public PluginEnabler
{
public:
    PluginListFiller(wxDialog *parent, wxControl *lab, wxChoice *ch) :
	PluginEnabler(), txt(lab), dlg(parent), plugins(), filtch(ch) {}
    PluginListFiller(const PluginListFiller &e) :
	PluginEnabler(), txt(e.txt), dlg(e.dlg), plugins(e.plugins),
        filtch(e.filtch) {}
    wxObject *Clone() const { return new PluginListFiller(*this); }
    bool Validate(wxWindow *p) { return true; }
    bool TransferToWindow()
    {
	PluginEnabler::TransferToWindow();
	wxChoice *ch = wxStaticCast(GetWindow(), wxChoice);
	ch->Clear();
	ch->Append(_("None"));
	plugins.clear();
	const wxString &plpath = wxStandardPaths::Get().GetPluginsDir();
	wxDir::GetAllFiles(plpath, &plugins, wxT("*.rpi"));
	for(int i = 0; i < plugins.size(); i++) {
	    wxDynamicLibrary dl(plugins[i], wxDL_VERBATIM|wxDL_NOW);
	    RENDPLUG_GetInfo GetInfo;
	    const RENDER_PLUGIN_INFO *rpi;
	    if(dl.IsLoaded() &&
	       (GetInfo = (RENDPLUG_GetInfo)dl.GetSymbol(wxT("RenderPluginGetInfo"))) &&
	       // note that in actual kega fusion plugins, rpi->Output is
	       // unused (as is rpi->Handle)
	       dl.GetSymbol(wxT("RenderPluginOutput")) &&
	       (rpi = GetInfo()) &&
	       // FIXME: maybe this should be >= RPI_VERISON
	       (rpi->Flags & 0xff) == RPI_VERSION &&
	       // RPI_565_SUPP is not supported
	       // although it would be possible
	       // and it would make Cairo more efficient
	       (rpi->Flags & (RPI_555_SUPP|RPI_888_SUPP))) {
		wxFileName fn(plugins[i]);
		wxString s = fn.GetName();
		s += wxT(": ");
		s += wxString(rpi->Name, wxConvUTF8, sizeof(rpi->Name));
		fn.MakeRelativeTo(plpath);
		plugins[i] = fn.GetFullName();
		ch->Append(s);
		if(plugins[i] == gopts.filter_plugin)
		    ch->SetSelection(i + 1);
	    } else
		plugins.RemoveAt(i--);
	}
	if(ch->GetCount() == 1) {
	    // this is probably the only place the user can find out where
	    // to put the plugins...  it depends on where program was
	    // installed, and of course OS
	    wxString msg;
	    msg.Printf(_("No usable rpi plugins found in %s"), plpath.c_str());
	    systemScreenMessage(msg);
	    ch->Hide();
	    txt->Hide();
	    int cursel = filtch->GetSelection();
	    if(cursel == FF_PLUGIN)
		cursel = 0;
	    if(filtch->GetCount() == FF_PLUGIN + 1) {
		filtch->Delete(FF_PLUGIN);
		// apparently wxgtk loses selection after this, even
		// if selection was not FF_PLUGIN
		filtch->SetSelection(cursel);
	    }
	} else {
	    ch->Show();
	    txt->Show();
	    if(filtch->GetCount() < FF_PLUGIN + 1)
		filtch->Append(_("Plugin"));
	}
	// FIXME: this isn't enough.  It only resizes 2nd time around
	dlg->Fit();
	return true;
    }
    bool TransferFromWindow()
    {
	wxChoice *ch = wxStaticCast(GetWindow(), wxChoice);
	if(ch->GetCount() == 1) {
	    gopts.filter_plugin = wxEmptyString;
	    // this happens if "Plugin" was selected and the entry was
	    // subsequently removed
	    if(ch->GetSelection() < 0)
		ch->SetSelection(0);
	    if(gopts.filter < 0)
		gopts.filter = 0;
	} else {
	    int n = ch->GetSelection();
	    if(n > 0)
		gopts.filter_plugin = plugins[n - 1];
	    else {
		if(filtch->GetSelection() == FF_PLUGIN) {
		    wxMessageBox(_("Please select a plugin or a different filter"),
				 _("Plugin selection error"), wxOK|wxICON_ERROR);
		    return false;
		}
		gopts.filter_plugin = wxEmptyString;
	    }
	}
	return true;
    }
private:
    wxDialog *dlg;
    wxControl *txt;
    wxChoice *filtch;
    wxArrayString plugins;
};

// this is the cmd table index for the accel tree ctrl
// one of the "benefits" of using TreeItemData is that we have to
// malloc them all, because treectrl destructor will free them all
// that means we can't use e.g. a single static table of len ncmds
class TreeInt : public wxTreeItemData
{
public:
    TreeInt(int i) : wxTreeItemData() { val = i; }
    int val;
};

// Convert a tree selection ID to a name
// root
//   parent
//     item
static bool treeid_to_name(int id, wxString &name, wxTreeCtrl *tc,
			   const wxTreeItemId &parent, int lev = 0)
{
    wxTreeItemIdValue cookie;
    for(wxTreeItemId tid = tc->GetFirstChild(parent, cookie); tid.IsOk();
	tid = tc->GetNextChild(parent, cookie)) {
	const TreeInt *ti = static_cast<const TreeInt *>(tc->GetItemData(tid));
	if(ti && ti->val == id) {
	    name = wxString(wxT(' '), 2 * lev) + tc->GetItemText(tid);
	    return true;
	}
	if(treeid_to_name(id, name, tc, tid, lev + 1)) {
	    name = wxString(wxT(' '), 2 * lev) + tc->GetItemText(tid) + wxT('\n') + name;
	    return true;
	}
    }
    return false;
}
			   
// for sorting accels by command ID
static bool cmdid_lt(const wxAcceleratorEntry &a, const wxAcceleratorEntry &b)
{
    return a.GetCommand() < b.GetCommand();
}

// manage the accel editor dialog
static class AccelConfig_t : public wxEvtHandler
{
public:
    wxTreeCtrl *tc;
    wxControlWithItems *lb;
    wxAcceleratorEntry_v user_accels, accels;
    wxWindow *asb, *remb;
    wxKeyTextCtrl *key;
    wxControl *curas;

    // since this is not the actual dialog, derived from wxDialog, which is
    // the normal way of doing things, do init on the show event instead of
    // constructor
    void Init(wxShowEvent &ev)
    {
#if wxCHECK_VERSION(2,9,0)
#define GetShow IsShown
#endif
	ev.Skip();
	if(!ev.GetShow())
	    return;
	lb->Clear();
	tc->Unselect();
	tc->ExpandAll();
	user_accels = gopts.accels;
	key->SetValue(wxT(""));
	asb->Enable(false);
	remb->Enable(false);
	curas->SetLabel(wxT(""));
	accels = wxGetApp().frame->get_accels(user_accels);
    }

    // on OK, save the accels in gopts
    void Set(wxCommandEvent &ev)
    {
	// opts.cpp assumes that gopts.accels entries with same command ID
	// are contiguous, so sort first
	std::sort(gopts.accels.begin(), gopts.accels.end(), cmdid_lt);
	gopts.accels = user_accels;
	wxGetApp().frame->set_global_accels();
	ev.Skip();
    }

    // After selecting item in command list, fill in key list
    // and maybe enable asb
    void CommandSel(wxTreeEvent &ev)
    {
	// wxTreeCtrl *tc = wxStaticCast(evt.GetEventObject(), wxTreeCtrl);
	// can't use wxStaticCast; wxTreeItemData does not derive from wxObject
	const TreeInt *id = static_cast<const TreeInt *>(tc->GetItemData(ev.GetItem()));
	if(!id) {
	    ev.Veto();
	    return;
	}
	if(ev.GetEventType() == wxEVT_COMMAND_TREE_SEL_CHANGING) {
	    ev.Skip();
	    return;
	}
	lb->Clear();
	remb->Enable(false);
	asb->Enable(!key->GetValue().empty());
	int cmd = id->val;

	for(int i = 0; i < accels.size(); i++)
	    if(accels[i].GetCommand() == cmdtab[cmd].cmd_id)
		lb->Append(wxKeyTextCtrl::ToString(accels[i].GetFlags(),
						   accels[i].GetKeyCode()));
    }

    // after selecting a key in key list, enable Remove button
    void KeySel(wxCommandEvent &ev)
    {
	remb->Enable(lb->GetSelection() != wxNOT_FOUND);
    }

    // remove selected binding
    void Remove(wxCommandEvent &ev)
    {
	int lsel = lb->GetSelection();
	if(lsel == wxNOT_FOUND)
	    return;
	wxString selstr = lb->GetString(lsel);
	int selmod, selkey;
	if(!wxKeyTextCtrl::FromString(selstr, selmod, selkey))
	    return; // this should never happen
	remb->Enable(false);
	// if this key is currently in the shortcut field, clear out curas
	if(selstr == key->GetValue())
	    curas->SetLabel(wxT(""));
	lb->Delete(lsel);
	// first drop from user accels, if applicable
	for(wxAcceleratorEntry_v::iterator i = user_accels.begin();
	    i < user_accels.end(); i++)
	    if(i->GetFlags() == selmod && i->GetKeyCode() == selkey) {
		user_accels.erase(i);
		break;
	    }
	// if it's a system accel, disable by assigning to NOOP
	wxAcceleratorEntry_v &sys_accels = wxGetApp().frame->sys_accels;
	for(int i = 0; i < sys_accels.size(); i++)
	    if(sys_accels[i].GetFlags() == selmod &&
	       sys_accels[i].GetKeyCode() == selkey) {
		wxAcceleratorEntry ne(selmod, selkey, XRCID("NOOP"));
		user_accels.push_back(ne);
	    }
	// finally, remove from accels instead of recomputing
	for(wxAcceleratorEntry_v::iterator i = accels.begin();
	    i < accels.end(); i++)
	    if(i->GetFlags() == selmod && i->GetKeyCode() == selkey) {
		accels.erase(i);
		break;
	    }
    }

    // wipe out all user bindings
    void ResetAll(wxCommandEvent &ev)
    {
	if(user_accels.empty() ||
	   wxMessageBox(_("This will clear all user-defined accelerators.  Are you sure?"),
			_("Confirm"), wxYES_NO) != wxYES)
	    return;
	user_accels.clear();
	accels = wxGetApp().frame->sys_accels;
	tc->Unselect();
	lb->Clear();
	// rather than recomputing curas, just clear it
	key->SetValue(wxT(""));
	curas->SetLabel(wxT(""));
    }

    // remove old key binding, add new key binding, and update GUI
    void Assign(wxCommandEvent &ev)
    {
	wxTreeItemId csel = tc->GetSelection();
	wxString accel = key->GetValue();
	if(!csel.IsOk() || accel.empty())
	    return;
	int acmod, ackey;
	if(!wxKeyTextCtrl::FromString(accel, acmod, ackey))
	    return; // this should never happen
	for(int i = 0; i < lb->GetCount(); i++)
	    if(lb->GetString(i) == accel)
		return; // ignore attempts to add twice
	lb->Append(accel);

	// first drop from user accels, if applicable
	for(wxAcceleratorEntry_v::iterator i = user_accels.begin();
	    i < user_accels.end(); i++)
	    if(i->GetFlags() == acmod && i->GetKeyCode() == ackey) {
		user_accels.erase(i);
		break;
	    }
	// then assign to this command
	const TreeInt *id = static_cast<const TreeInt *>(tc->GetItemData(csel));
	wxAcceleratorEntry ne(acmod, ackey, cmdtab[id->val].cmd_id);
	user_accels.push_back(ne);

	// now assigned to this cmd...
	wxString lab;
	treeid_to_name(id->val, lab, tc, tc->GetRootItem());
	curas->SetLabel(lab);

	// finally, instead of recomputing accels, just append new accel
	accels.push_back(ne);
    }

    // update curas and maybe enable asb
    void CheckKey(wxCommandEvent &ev)
    {
	wxString nkey = key->GetValue();
	if(nkey.empty()) {
	    curas->SetLabel(wxT(""));
	    asb->Enable(false);
	    return;
	}
	int acmod, ackey;
	if(!wxKeyTextCtrl::FromString(nkey, acmod, ackey)) {
	    // this should never happen
	    key->SetValue(wxT(""));
	    asb->Enable(false);
	    return;
	}
	asb->Enable(tc->GetSelection().IsOk());
	int cmd = -1;
	for(int i = 0; i < accels.size(); i++)
	    if(accels[i].GetFlags() == acmod && accels[i].GetKeyCode() == ackey) {
		int cmdid = accels[i].GetCommand();
		for(cmd = 0; cmd < ncmds; cmd++)
		    if(cmdid == cmdtab[cmd].cmd_id)
			break;
		break;
	    }
	if(cmd < 0 || cmdtab[cmd].cmd_id == XRCID("NOOP")) {
	    curas->SetLabel(wxT(""));
	    return;
	}
	wxString lab;
	treeid_to_name(cmd, lab, tc, tc->GetRootItem());
	curas->SetLabel(lab);
    }
} accel_config_handler;

// build initial accel tree control from menu
void MainFrame::add_menu_accels(wxTreeCtrl *tc, wxTreeItemId &parent, wxMenu *menu)
{
    wxMenuItemList mil = menu->GetMenuItems();
    for(wxMenuItemList::iterator mi = mil.begin(); mi != mil.end(); mi++) {
	if((*mi)->IsSeparator()) {
	    tc->AppendItem(parent, wxT("-----"));
	} else if((*mi)->IsSubMenu()) {
	    wxTreeItemId id = tc->AppendItem(parent, (*mi)->GetItemLabelText());
	    add_menu_accels(tc, id, (*mi)->GetSubMenu());
	    if((*mi)->GetSubMenu() == recent) {
		for(int i = wxID_FILE1; i <= wxID_FILE10; i++) {
		    int cmdid;
		    for(cmdid = 0; cmdid < ncmds; cmdid++)
			if(cmdtab[cmdid].cmd_id == i)
			    break;
		    TreeInt *val = new TreeInt(cmdid);
		    tc->AppendItem(id, cmdtab[cmdid].name, -1, -1, val);
		}
	    }
	} else {
	    int mid = (*mi)->GetId();
	    if(mid >= wxID_FILE1 && mid <= wxID_FILE10)
		continue;
	    int cmdid;
	    for(cmdid = 0; cmdid < ncmds; cmdid++)
		if(cmdtab[cmdid].cmd_id == mid)
		    break;
	    if(cmdid == ncmds)
		continue; // bad menu item; should inform user really
	    TreeInt *val = new TreeInt(cmdid);
	    // ugh.  There has to be a better way...
	    // perhaps make XRCID ranges a requirement for load/save st?
	    // but then if the user overides main menu, that req. is broken..
	    wxString txt = (*mi)->GetItemLabelText();
	    for(int i = 0; i < 10; i++)
		if(*mi == loadst_mi[i] || *mi == savest_mi[i]) {
		    txt = cmdtab[i].name;
		    break;
		}
	    tc->AppendItem(parent, txt, -1, -1, val);
	}
    }
}

// manage throttle spinctrl/canned setting choice interaction
static class ThrottleCtrl_t : public wxEvtHandler
{
public:
    wxSpinCtrl *thr;
    wxChoice *thrsel;

    // set thrsel from thr
    void SetThrottleSel(wxSpinEvent &evt)
    {
	DoSetThrottleSel(thr->GetValue());
    }

    void DoSetThrottleSel(int val)
    {
	switch(val) {
	case 0:
	    thrsel->SetSelection(1);
	    break;
	case 25:
	    thrsel->SetSelection(2);
	    break;
	case 50:
	    thrsel->SetSelection(3);
	    break;
	case 100:
	    thrsel->SetSelection(4);
	    break;
	case 150:
	    thrsel->SetSelection(5);
	    break;
	case 200:
	    thrsel->SetSelection(6);
	    break;
	default:
	    thrsel->SetSelection(0);
	    break;
	}
    }

    // set thr from thrsel
    void SetThrottle(wxCommandEvent &evt)
    {
	switch(thrsel->GetSelection()) {
	case 0: // blank; leave it alone
	    break;
	case 1:
	    thr->SetValue(0);
	    break;
	case 2:
	    thr->SetValue(25);
	    break;
	case 3:
	    thr->SetValue(50);
	    break;
	case 4:
	    thr->SetValue(100);
	    break;
	case 5:
	    thr->SetValue(150);
	    break;
	case 6:
	    thr->SetValue(200);
	    break;
	}
    }

    // since this is not the actual dialog, derived from wxDialog, which is
    // the normal way of doing things, do init on the show event instead of
    // constructor
    // could've also used a validator, I guess...
    void Init(wxShowEvent &ev)
    {
	ev.Skip();
	DoSetThrottleSel(gopts.throttle);
    }
} throttle_ctrl;

/////////////////////////////

bool MainFrame::InitMore(void)
{
    // Make sure display panel present and correct type
    panel = XRCCTRL(*this, "DisplayArea", GameArea);
    if(!panel) {
	wxLogError(_("Main display panel not found"));
	return false;
    }
    panel->AdjustSize(false);

    // only the panel does idle events (the emulator loop)
    // however, do not enable until end of init, since errors will start
    // the idle loop on wxGTK
    wxIdleEvent::SetMode(wxIDLE_PROCESS_SPECIFIED);

    // could/should probably take this from xrc as well
    // but I don't think xrc supports icon from Windows resource
    wxIcon icon = wxXmlResource::Get()->LoadIcon(wxT("MainIcon"));
    if(!icon.IsOk()) {
	wxLogInfo(_("Main icon not found"));
	icon = wxICON(wxvbam);
    }
    SetIcon(icon);

    // NOOP if no status area
    SetStatusText(_("Welcome to wxVBAM!"));

    // Prepare system accel table
    for(int i = 0; i < num_def_accels; i++)
	sys_accels.push_back(default_accels[i]);

    // If there is a menubar, store all special menuitems
#define XRCITEM_I(id) menubar->FindItem(id, NULL)
#define XRCITEM_D(s) XRCITEM_I(XRCID_D(s))
#define XRCITEM(s) XRCITEM_D(wxT(s))
    wxMenuBar *menubar = GetMenuBar();
    ctx_menu = NULL;
    if(menubar) {
#if 0 // doesn't work in 2.9 at all (causes main menu to malfunction)
	// to fix, recursively copy entire menu insted of just copying
	// menubar.  This means that every saved menu item must also be
	// saved twice...  A lot of work for a mostly worthless feature.
	// If you want the menu, just exit full-screen mode.
	// Either that, or add an option to retain the regular
	// menubar in full-screen mode

	// create a context menu for fullscreen mode
	// FIXME: on gtk port, this gives Gtk-WARNING **:
	//   gtk_menu_attach_to_widget(): menu already attached to GtkMenuItem
	// but it works anyway
	// Note: menu default accelerators (e.g. alt-f for file menu) don't
	// work with context menu (and can never work, since there is no
	// way to pop up a submenu)
	// It would probably be better, in the end, to use a collapsed menu
	// bar (either Amiga-style press RMB to make appear, or Windows
	// collapsed toolbar-style move mouse to within a pixel of top to
	// make appear).  Not supported in wx without a lot of work, though.
	// Maybe this feature should just be dropped; the user would simply
	// have to exit fullscreen mode to use the menu.
	ctx_menu = new wxMenu();
	for(int i = 0; i < menubar->GetMenuCount(); i++)
	    ctx_menu->AppendSubMenu(menubar->GetMenu(i), menubar->GetMenuLabel(i));
#endif

	// save all menu items in the command table
	for(int i = 0; i < ncmds; i++) {
	    wxMenuItem *mi = cmdtab[i].mi = XRCITEM_I(cmdtab[i].cmd_id);
	    // remove unsupported commands first
#ifdef NO_FFMPEG
	    if(cmdtab[i].mask_flags & (CMDEN_SREC|CMDEN_NSREC|CMDEN_VREC|CMDEN_NVREC)) {
		if(mi)
		    mi->GetMenu()->Remove(mi);
		cmdtab[i].cmd_id = XRCID("NOOP");
		cmdtab[i].mi = NULL;
		continue;
	    }
#endif
#ifndef GBA_LOGGING
	    if(cmdtab[i].cmd_id == XRCID("Logging")) {
		if(mi)
		    mi->GetMenu()->Remove(mi);
		cmdtab[i].cmd_id = XRCID("NOOP");
		cmdtab[i].mi = NULL;
		continue;
	    }
#endif
#ifdef NO_LINK
	    if(cmdtab[i].cmd_id == XRCID("LinkConfigure") ||
	       cmdtab[i].cmd_id == XRCID("LanLink")) {
		if(mi)
		    mi->GetMenu()->Remove(mi);
		cmdtab[i].cmd_id = XRCID("NOOP");
		cmdtab[i].mi = NULL;
		continue;
	    }
#endif
	    if(mi) {
		// wxgtk provides no way to retrieve stock label/accel
		// and does not override wxGetStockLabel()
		// as of 2.8.12/2.9.1
		// so override with wx's stock label  <sigh>
		// at least you still get gtk's stock icon
		if(mi->GetItemLabel().empty())
		    mi->SetItemLabel(wxGetStockLabel(mi->GetId(),
						     wxSTOCK_WITH_MNEMONIC|wxSTOCK_WITH_ACCELERATOR));

		// add accelerator to global accel table
		wxAcceleratorEntry *a = mi->GetAccel();
		if(a) {
		    a->Set(a->GetFlags(), a->GetKeyCode(), cmdtab[i].cmd_id, mi);
		    // only add it if not already there
		    for(wxAcceleratorEntry_v::iterator e = sys_accels.begin();
			e < sys_accels.end(); e++)
			if(a->GetFlags() == e->GetFlags() &&
			   a->GetKeyCode() == e->GetKeyCode()) {
			    if(e->GetMenuItem()) {
				wxLogInfo(_("Duplicate menu accelerator: %s for %s and %s; keeping first"),
					  wxKeyTextCtrl::ToString(a->GetFlags(), a->GetKeyCode()).c_str(),
					  e->GetMenuItem()->GetItemLabelText().c_str(),
					  mi->GetItemLabelText().c_str());
				delete a;
				a = 0;
			    } else {
				if(e->GetCommand() != a->GetCommand()) {
				    int cmd;
				    for(cmd = 0; cmd < ncmds; cmd++)
					if(cmdtab[cmd].cmd_id == e->GetCommand())
					    break;
				    wxLogInfo(_("Menu accelerator %s for %s overrides default for %s ; keeping menu"),
					      wxKeyTextCtrl::ToString(a->GetFlags(), a->GetKeyCode()).c_str(),
					      mi->GetItemLabelText().c_str(),
					      cmdtab[cmd].cmd);
				}
				sys_accels.erase(e);
			    }
			    break;
			}
		    if(a)
			sys_accels.push_back(*a);
		    else
			// strip from label so user isn't confused
			DoSetAccel(mi, NULL);
		}

		// store checkable items
		if(mi->IsCheckable()) {
		    checkable_mi_t cmi = { cmdtab[i].cmd_id, mi };
		    checkable_mi.push_back(cmi);
		}
	    }
	}

        // if a recent menu is present, save its location
	wxMenuItem *recentmi = XRCITEM("RecentMenu");
	if(recentmi && recentmi->IsSubMenu()) {
	    recent = recentmi->GetSubMenu();
	    gopts.recent->UseMenu(recent);
	    gopts.recent->AddFilesToMenu();
	} else
	    recent = NULL;
	// if save/load state menu items present, save their locations
	for(int i = 0; i < 10; i++) {
	    wxString n;
	    n.Printf(wxT("LoadGame%02d"),  i + 1);
	    loadst_mi[i] = XRCITEM_D(n);
	    n.Printf(wxT("SaveGame%02d"), i + 1);
	    savest_mi[i] = XRCITEM_D(n);
	}
    } else {
	recent = NULL;
	for(int i = 0; i < 10; i++)
	    loadst_mi[i] = savest_mi[i] = NULL;
    }
    // just setting to UNLOAD_CMDEN_KEEP is invalid
    // so just set individual flags here
    cmd_enable = CMDEN_NGDB_ANY | CMDEN_NREC_ANY;
    update_state_ts(true);
    enable_menus();
    // set pointers for checkable menu items
    // and set initial checked status
    if(checkable_mi.size()) {
#define add_bcheck(s, f) do { \
    int id = XRCID(s); \
    for(int i = 0; i < checkable_mi.size(); i++) { \
	if(checkable_mi[i].cmd != id) \
	    continue; \
	checkable_mi[i].boolopt = &f; \
	checkable_mi[i].mi->Check(f); \
	break; \
    } \
} while(0)

#define add_icheck(s, f, m, v) do { \
    int id = XRCID(s); \
    for(int i = 0; i < checkable_mi.size(); i++) { \
	if(checkable_mi[i].cmd != id) \
	    continue; \
	checkable_mi[i].intopt = &f; \
	checkable_mi[i].mask = m; \
	checkable_mi[i].val = v; \
	checkable_mi[i].mi->Check((f & m) == v); \
	break; \
    } \
} while(0)
#define add_icheck1(s, f, m) add_icheck(s, f, m, m)
	add_bcheck("RecentFreeze", gopts.recent_freeze);
	add_bcheck("Pause", paused);
	add_icheck1("SoundChannel1", gopts.sound_en, (1<<0));
	add_icheck1("SoundChannel2", gopts.sound_en, (1<<1));
	add_icheck1("SoundChannel3", gopts.sound_en, (1<<2));
	add_icheck1("SoundChannel4", gopts.sound_en, (1<<3));
	add_icheck1("DirectSoundA", gopts.sound_en, (1<<8));
	add_icheck1("DirectSoundB", gopts.sound_en, (1<<9));
	add_icheck1("VideoLayersBG0", layerSettings, (1<<8));
	add_icheck1("VideoLayersBG1", layerSettings, (1<<9));
	add_icheck1("VideoLayersBG2", layerSettings, (1<<10));
	add_icheck1("VideoLayersBG3", layerSettings, (1<<11));
	add_icheck1("VideoLayersOBJ", layerSettings, (1<<12));
	add_icheck1("VideoLayersWIN0", layerSettings, (1<<13));
	add_icheck1("VideoLayersWIN1", layerSettings, (1<<14));
	add_icheck1("VideoLayersOBJWIN", layerSettings, (1<<15));
	add_bcheck("CheatsAutoSaveLoad", gopts.autoload_cheats);
	add_bcheck("CheatsEnable", cheatsEnabled);
	add_bcheck("KeepSaves", skipSaveGameBattery);
	add_bcheck("KeepCheats", skipSaveGameCheats);
	add_bcheck("LoadGameAutoLoad", gopts.autoload_state);
	add_icheck1("JoypadAutofireA", autofire, KEYM_A);
	add_icheck1("JoypadAutofireB", autofire, KEYM_B);
	add_icheck1("JoypadAutofireL", autofire, KEYM_LEFT);
	add_icheck1("JoypadAutofireR", autofire, KEYM_RIGHT);
	add_bcheck("EmulatorSpeedupToggle", turbo);
    }
    for(int i = 0; i < checkable_mi.size(); i++)
	if(!checkable_mi[i].boolopt && !checkable_mi[i].intopt) {
	    wxLogError(_("Invalid menu item %s; removing"),
		       checkable_mi[i].mi->GetItemLabelText().c_str());
	    checkable_mi[i].mi->GetMenu()->Remove(checkable_mi[i].mi);
	    checkable_mi[i].mi = NULL;
	}
    for(checkable_mi_array_t::iterator it = checkable_mi.end();
	it != checkable_mi.begin(); it--)
	if(!it[-1].mi)
	    checkable_mi.erase(it-1);

    set_global_accels();

    // preload and verify all resource dialogs
    // this will take init time and memory, but catches errors in xrc sooner
    // note that the only verification done is to ensure no crashes.  It's the
    // user's responsibility to ensure that the GUI works as intended after
    // modifications

    wxDialog *d = 0;
    const wxChar *dname;
#define baddialog() do { \
    wxLogError(_("Unable to load dialog %s from resources"), dname); \
    return false; \
} while(0)
#define baddialogcv(n) do { \
    wxLogError(_("Unable to load dialog %s (control %s) from resources"), dname, n); \
    return false; \
} while(0)
#define baddialogc(n) baddialogcv(wxT(n))
#define LoadXRCDialog(n) do { \
    /* why do I have to manually Fit()? */ \
    /* since I do, always do it for last item so other init happens first */ \
    /* don't forget to Fit() the last dialog! */ \
    if(d != 0) \
	d->Fit(); \
    dname = wxT(n); \
    /* using this instead of LoadDialog() allows non-wxDialog classes that */ \
    /* are derived from wxDialog (like wxPropertyDialog) to work */ \
    d = wxDynamicCast(wxXmlResource::Get()->LoadObject(this, dname, wxEmptyString), \
		      wxDialog); \
    if(!d) \
	baddialog(); \
    /* wx-2.9.1 doesn't set parent for propertysheetdialogs for some reason */ \
    /* this will generate a gtk warning but it is necessary for later */ \
    /* retrieval using FindWindow() */ \
    if(!d->GetParent()) \
	d->Reparent(this); \
 \
    mark_recursive(d); \
} while(0)

#define vfld(f, t) do { \
    if(!XRCCTRL(*d, f, t)) \
	baddialogc(f); \
} while(0)
#define getfld(v, f, t) do { \
    if(!(v = XRCCTRL(*d, f, t))) \
	baddialogc(f); \
} while(0)
#define getfldv(v, f, t) do { \
    if(!(v = XRCCTRL_D(*d, f, t))) \
	baddialogcv(f.c_str()); \
} while(0)

    //// displayed during run
    LoadXRCDialog("GBPrinter");
    // just verify preview window & mag sel present
    {
	wxPanel *prev;
	getfld(prev, "Preview", wxPanel);
	if(!wxDynamicCast(prev->GetParent(), wxScrolledWindow))
	    baddialogc("Preview");
	vfld("Magnification", wxControlWithItems);
    }

    //// File menu
    LoadXRCDialog("GBAROMInfo");
    // just verify fields present
    wxControl *lab;
#define getlab(n) getfld(lab, n, wxControl)
    getlab("Title");
    getlab("GameCode");
    getlab("MakerCode");
    getlab("MakerName");
    getlab("UnitCode");
    getlab("DeviceType");
    getlab("Version");
    getlab("CRC");

    LoadXRCDialog("GBROMInfo");
    // just verify fields present
    getlab("Title");
    getlab("MakerCode");
    getlab("MakerName");
    getlab("UnitCode");
    getlab("DeviceType");
    getlab("Version");
    getlab("CRC");
    getlab("Color");
    getlab("ROMSize");
    getlab("RAMSize");
    getlab("DestCode");
    getlab("LicCode");
    getlab("Checksum");

    LoadXRCDialog("CodeSelect");
    // just verify list present
    vfld("CodeList", wxControlWithItems);

    LoadXRCDialog("ExportSPS");
    // just verify text fields present
    vfld("Title", wxTextCtrl);
    vfld("Description", wxTextCtrl);
    vfld("Notes", wxTextCtrl);

    //// Emulation menu
#ifndef NO_LINK
    LoadXRCDialog("NetLink");
#endif
    wxRadioButton *rb;
#define getrbi(n, o, v) do { \
    getfld(rb, n, wxRadioButton); \
    rb->SetValidator(wxBoolIntValidator(&o, v)); \
} while(0)
#define getrbb(n, o) do { \
    getfld(rb, n, wxRadioButton); \
    rb->SetValidator(wxGenericValidator(&o)); \
} while(0)
#define getrbbr(n, o) do { \
    getfld(rb, n, wxRadioButton); \
    rb->SetValidator(wxBoolRevValidator(&o)); \
} while(0)
    wxBoolEnValidator *benval;
    wxBoolEnHandler *ben;
#define getbe(n, o, cv, t, wt) do { \
    getfld(cv, n, t); \
    cv->SetValidator(wxBoolEnValidator(&o)); \
    benval = wxStaticCast(cv->GetValidator(), wxBoolEnValidator); \
    static wxBoolEnHandler _ben; \
    ben = &_ben; \
    wx##wt##BoolEnHandlerConnect(cv, wxID_ANY, _ben); \
} while(0)
    // brenval & friends are here just to allow yes/no radioboxes in place
    // of checkboxes.  A lot of work for little benefit.
    wxBoolRevEnValidator *brenval;
#define getbre(n, o, cv, t, wt) do { \
    getfld(cv, n, t); \
    cv->SetValidator(wxBoolRevEnValidator(&o)); \
    brenval = wxStaticCast(cv->GetValidator(), wxBoolRevEnValidator); \
    wx##wt##BoolEnHandlerConnect(rb, wxID_ANY, *ben); \
} while(0)
#define addbe(n) do { \
    ben->controls.push_back(n); \
    benval->controls.push_back(n); \
} while(0)
#define addrbe(n) do { \
    addbe(n); \
    brenval->controls.push_back(n); \
} while(0)
#define addber(n, r) do { \
    ben->controls.push_back(n); \
    ben->reverse.push_back(r); \
    benval->controls.push_back(n); \
    benval->reverse.push_back(r); \
} while(0)
#define addrber(n, r) do { \
    addber(n, r); \
    brenval->controls.push_back(n); \
    brenval->reverse.push_back(r); \
} while(0)
#define getrbbe(n, o) getbe(n, o, rb, wxRadioButton, RBE)
#define getrbbd(n, o) getbre(n, o, rb, wxRadioButton, RBD)
    wxTextCtrl *tc;
#define gettc(n, o) do { \
    getfld(tc, n, wxTextCtrl); \
    tc->SetValidator(wxTextValidator(wxFILTER_NONE, &o)); \
} while(0)
#ifndef NO_LINK
    {
	net_link_handler.dlg = d;
	getrbbe("Server", lanlink.server);
	getrbbd("Client", lanlink.server);
	getlab("PlayersLab");
	addrber(lab, false);
	getrbi("Link2P", net_link_handler.n_players, 2);
	addrber(rb, false);
	getrbi("Link3P", net_link_handler.n_players, 3);
	addrber(rb, false);
	getrbi("Link4P", net_link_handler.n_players, 4);
	addrber(rb, false);
	getlab("ServerIPLab");
	addrber(lab, true);
	gettc("ServerIP", gopts.link_host);
	addrber(tc, true);
	tc->SetValidator(IPHostValidator(&gopts.link_host));
	getrbbr("SpeedOff", lanlink.speed);
	getrbb("SpeedOn", lanlink.speed);
	wxWindow *okb = d->FindWindow(wxID_OK);
	if(okb) { // may be gone if style guidlines removed it
	    net_link_handler.okb = wxStaticCast(okb, wxButton);
	    d->Connect(XRCID("Server"), wxEVT_COMMAND_RADIOBUTTON_SELECTED,
		       wxCommandEventHandler(NetLink_t::ServerOKButton),
		       NULL, &net_link_handler);
	    d->Connect(XRCID("Client"), wxEVT_COMMAND_RADIOBUTTON_SELECTED,
		       wxCommandEventHandler(NetLink_t::ClientOKButton),
		       NULL, &net_link_handler);
	}
	// this should intercept wxID_OK before the dialog handler gets it
	d->Connect(wxID_OK, wxEVT_COMMAND_BUTTON_CLICKED,
		   wxCommandEventHandler(NetLink_t::NetConnect),
		   NULL, &net_link_handler);
    }
#endif

    LoadXRCDialog("CheatList");
    {
	cheat_list_handler.dlg = d;
	d->SetEscapeId(wxID_OK);
	wxCheckedListCtrl *cl;
	getfld(cl, "Cheats", wxCheckedListCtrl);
	if(!cl->Init())
	    baddialogc("Cheats");
	cheat_list_handler.list = cl;
	cl->SetValidator(CheatListFill());
	cl->InsertColumn(0, _("Code"));
	// can't just set font for whole column; must set in each
	// individual item
	wxFont of = cl->GetFont();
	// of.SetFamily(wxFONTFAMILY_MODERN);  // doesn't work (no font change)
	wxFont f(of.GetPointSize(), wxFONTFAMILY_MODERN, of.GetStyle(),
		 of.GetWeight());
	cheat_list_handler.item0.SetFont(f);
	cheat_list_handler.item0.SetColumn(0);
	cl->InsertColumn(1, _("Description"));
	// too bad I can't just set the size to windowwidth - other cols
	// default width is header width, but using following will probably
	// make it 80 pixels wide regardless
	// cl->SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER);
	cheat_list_handler.col1minw = cl->GetColumnWidth(1);
	// on wxGTK, column 1 seems to inherit column 0's font regardless
	// of requested font
	cheat_list_handler.item1.SetFont(cl->GetFont());
	cheat_list_handler.item1.SetColumn(1);
#if 0
	// the ideal way to set col 0's width would be to use
	// wxLIST_AUTOSIZE after setting value to a sample:
	cheat_list_handler.item0.SetText(wxT("00000000 00000000"));
	cl->InsertItem(cheat_list_handler.item0);
	cl->SetColumnWidth(0, wxLIST_AUTOSIZE);
	cl->RemoveItem(0);
#else
	// however, the generic listctrl implementation uses the wrong
	// font to determine width (window vs. item), and does not
	// calculate the margins the same way in calculation vs. actual
	// drawing.  so calculate manually, using knowledge of underlying
	// code.  This is highly version-unportable, but better than using
	// buggy wx code..
	int w, h;
	cl->GetImageList(wxIMAGE_LIST_SMALL)->GetSize(0, w, h);
	w += 5; // IMAGE_MARGIN_IN_REPORT_MODE
	// following is missing from wxLIST_AUTOSIZE
	w += 8; // ??? subtracted from width avail for text
	{
	    int charwidth, charheight;
	    wxClientDC dc(cl);
	    // following is item font instead of window font,
	    // and so is missing from wxLIST_AUTOSIZE
	    dc.SetFont(f);
	    dc.GetTextExtent(wxT('M'), &charwidth, &charheight);
	    w += (8 + 1 + 8) * charwidth;
	}
	cl->SetColumnWidth(0, w);
#endif

	d->Connect(wxEVT_COMMAND_TOOL_CLICKED,
		   wxCommandEventHandler(CheatList_t::Tool),
		   NULL, &cheat_list_handler);
	d->Connect(wxEVT_COMMAND_LIST_ITEM_CHECKED,
		   wxListEventHandler(CheatList_t::Check),
		   NULL, &cheat_list_handler);
	d->Connect(wxEVT_COMMAND_LIST_ITEM_UNCHECKED,
		   wxListEventHandler(CheatList_t::UnCheck),
		   NULL, &cheat_list_handler);
	d->Connect(wxEVT_COMMAND_LIST_ITEM_ACTIVATED,
		   wxListEventHandler(CheatList_t::Edit),
		   NULL, &cheat_list_handler);
    }

    LoadXRCDialog("CheatEdit");
    wxChoice *ch;
#define getch(n, o) do { \
    getfld(ch, n, wxChoice); \
    ch->SetValidator(wxGenericValidator(&o)); \
} while(0)
    {
	// d->Reparent(cheat_list_handler.dlg); // broken
	getch("Type", cheat_list_handler.ce_type);
	cheat_list_handler.ce_type_ch = ch;
	gettc("Desc", cheat_list_handler.ce_desc);
	tc->SetMaxLength(sizeof(cheatsList[0].desc) - 1);
	gettc("Codes", cheat_list_handler.ce_codes);
	cheat_list_handler.ce_codes_tc = tc;
    }

    LoadXRCDialog("CheatCreate");
    {
	cheat_find_handler.dlg = d;
	d->SetEscapeId(wxID_OK);
	CheatListCtrl *list;
	getfld(list, "CheatList", CheatListCtrl);
	cheat_find_handler.list = list;
	list->SetValidator(CheatFindFill());
	list->InsertColumn(0, _("Address"));
	list->InsertColumn(1, _("Old Value"));
	list->InsertColumn(2, _("New Value"));
	getrbi("EQ", cheat_find_handler.op, SEARCH_EQ);
	getrbi("NE", cheat_find_handler.op, SEARCH_NE);
	getrbi("LT", cheat_find_handler.op, SEARCH_LT);
	getrbi("LE", cheat_find_handler.op, SEARCH_LE);
	getrbi("GT", cheat_find_handler.op, SEARCH_GT);
	getrbi("GE", cheat_find_handler.op, SEARCH_GE);
#define cf_make_update() \
    rb->Connect(wxEVT_COMMAND_RADIOBUTTON_SELECTED, \
		wxCommandEventHandler(CheatFind_t::UpdateView), \
		NULL, &cheat_find_handler)
	getrbi("Size8", cheat_find_handler.size, BITS_8);
	cf_make_update();
	getrbi("Size16", cheat_find_handler.size, BITS_16);
	cf_make_update();
	getrbi("Size32", cheat_find_handler.size, BITS_32);
	cf_make_update();
	getrbi("Signed", cheat_find_handler.fmt, CFVFMT_SD);
	cf_make_update();
	getrbi("Unsigned", cheat_find_handler.fmt, CFVFMT_UD);
	cf_make_update();
	getrbi("Hexadecimal", cheat_find_handler.fmt, CFVFMT_UH);
	cf_make_update();
#define cf_make_valen() \
    rb->Connect(wxEVT_COMMAND_RADIOBUTTON_SELECTED, \
		wxCommandEventHandler(CheatFind_t::EnableVal), \
		NULL, &cheat_find_handler)
	getrbi("OldValue", cheat_find_handler.valsrc, 1);
	cf_make_valen();
	cheat_find_handler.old_rb = rb;
	rb->Disable();
	getrbi("SpecificValue", cheat_find_handler.valsrc, 0);
	cf_make_valen();
	cheat_find_handler.val_rb = rb;
	gettc("Value", cheat_find_handler.val_s);
	cheat_find_handler.val_tc = tc;
	wxStaticCast(tc->GetValidator(), wxTextValidator)->SetStyle(wxFILTER_INCLUDE_CHAR_LIST);
#define cf_button(n, f) \
    d->Connect(XRCID(n), wxEVT_COMMAND_BUTTON_CLICKED, \
	       wxCommandEventHandler(CheatFind_t::f), \
	       NULL, &cheat_find_handler);
#define cf_enbutton(n, v) do { \
    getfld(cheat_find_handler.v, n, wxButton); \
    cheat_find_handler.v->Disable(); \
} while(0)
	cf_button("Search", Search);
	cf_button("Update", UpdateVals);
	cf_enbutton("Update", update_b);
	cf_button("Clear", ResetSearch);
	cf_enbutton("Clear", clear_b);
	cf_button("AddCheat", AddCheatB);
	cf_enbutton("AddCheat", add_b);
	d->Connect(wxEVT_COMMAND_LIST_ITEM_ACTIVATED,
		   wxListEventHandler(CheatFind_t::AddCheatL),
		   NULL, &cheat_find_handler);
	d->Connect(wxEVT_COMMAND_LIST_ITEM_SELECTED,
		   wxListEventHandler(CheatFind_t::Select),
		   NULL, &cheat_find_handler);
    }

    LoadXRCDialog("CheatAdd");
    {
	// d->Reparent(cheat_find_handler.dlg); // broken
	gettc("Desc", cheat_find_handler.ca_desc);
	tc->SetMaxLength(sizeof(cheatsList[0].desc) - 1);
	gettc("Value", cheat_find_handler.ca_val);
	cheat_find_handler.ca_val_tc = tc;
	// MFC interface used this for cheat list's generic code adder as well,
	// and made format selectable in interface.  I think the plain
	// interface is good enough, even though the format for GB cheats
	// is non-obvious.  Therefore, the format is now just a read-only
	// field.
	getlab("Format");
	cheat_find_handler.ca_fmt = lab;
	getlab("Address");
	cheat_find_handler.ca_addr = lab;
    }

    //// config menu
    LoadXRCDialog("GeneralConfig");
    wxCheckBox *cb;
#define getcbb(n, o) do { \
    getfld(cb, n, wxCheckBox); \
    cb->SetValidator(wxGenericValidator(&o)); \
} while(0)
    wxSpinCtrl *sc;
#define getsc(n, o) do { \
    getfld(sc, n, wxSpinCtrl); \
    sc->SetValidator(wxGenericValidator(&o)); \
} while(0)
    {
	getcbb("PauseWhenInactive", gopts.defocus_pause);
	getcbb("ApplyPatches", gopts.apply_patches);
	getrbi("PNG", gopts.cap_format, 0);
	getrbi("BMP", gopts.cap_format, 1);
	getsc("RewindInterval", gopts.rewind_interval);
	getsc("Throttle", gopts.throttle);
	throttle_ctrl.thr = sc;
	getfld(throttle_ctrl.thrsel, "ThrottleSel", wxChoice);
	throttle_ctrl.thr->
	    Connect(wxEVT_COMMAND_SPINCTRL_UPDATED,
		    wxSpinEventHandler(ThrottleCtrl_t::SetThrottleSel),
		    NULL, &throttle_ctrl);
	throttle_ctrl.thrsel->
	    Connect(wxEVT_COMMAND_CHOICE_SELECTED,
		    wxCommandEventHandler(ThrottleCtrl_t::SetThrottle),
		    NULL, &throttle_ctrl);
	d->Connect(wxEVT_SHOW, wxShowEventHandler(ThrottleCtrl_t::Init),
		   NULL, &throttle_ctrl);
    }

#define getcbbe(n, o) getbe(n, o, cb, wxCheckBox, CB)
    wxBoolIntEnValidator *bienval;
#define getbie(n, o, v, cv, t, wt) do { \
    getfld(cv, n, t); \
    cv->SetValidator(wxBoolIntEnValidator(&o, v, v)); \
    bienval = wxStaticCast(cv->GetValidator(), wxBoolIntEnValidator); \
    static wxBoolEnHandler _ben; \
    ben = &_ben; \
    wx##wt##BoolEnHandlerConnect(cv, wxID_ANY, _ben); \
} while(0)
#define addbie(n) do { \
    ben->controls.push_back(n); \
    bienval->controls.push_back(n); \
} while(0)
#define addbier(n, r) do { \
    ben->controls.push_back(n); \
    ben->reverse.push_back(r); \
    bienval->controls.push_back(n); \
    bienval->reverse.push_back(r); \
} while(0)
#define getcbie(n, o, v) getbie(n, o, v, cb, wxCheckBox, CB)
    wxFilePickerCtrl *fp;
#define getfp(n, o) do { \
    getfld(fp, n, wxFilePickerCtrl); \
    fp->SetValidator(wxFileDirPickerValidator(&o)); \
} while(0)
    LoadXRCDialog("GameBoyConfig");
    {
	/// System and Peripherals
	getch("System", gbEmulatorType);
	// "Display borders" corresponds to 2 variables, so it is handled
	// in command handler.  Plus making changes might require resizing
	// game area.  Validation only here.
	vfld("Borders", wxChoice);
	getcbbe("Printer", gopts.gbprint);
	getcbb("PrintGather", gopts.print_auto_page);
	addbe(cb);
	getcbb("PrintSnap", gopts.print_screen_cap);
	addbe(cb);
	/// Speed
	// AutoSkip/FrameSkip are 2 controls for 1 value.  Needs post-process
	// to ensure checkbox not ignored
	getcbie("FrameSkipAuto", gopts.gb_frameskip, -1);
	getsc("FrameSkip", gopts.gb_frameskip);
	addbier(sc, true);
	getlab("FrameSkipLab");
	addbier(lab, true);
	/// Boot ROM
	getcbbe("BootRomEn", gopts.gb_use_bios);
	getfp("BootRom", gopts.gb_bios);
	addbe(fp);
	getlab("BootRomLab");
	addbe(lab);
	getcbbe("CBootRomEn", gopts.gbc_use_bios);
	getfp("CBootRom", gopts.gbc_bios);
	addbe(fp);
	getlab("CBootRomLab");
	addbe(lab);
	/// Custom Colors
	getcbb("Color", gbColorOption);
	wxFarRadio *r = 0;
	for(int i = 0; i < 3; i++) {
	    wxString pn;
	    // NOTE: wx2.9.1 behaves differently for referenced nodes
	    // than 2.8!  Unless there is an actual child node, the ID field
	    // will not be overwritten.  This means that there should be a
	    // dummy child node (e.g. position=(0,0)).  If you get
	    // "Unable to load dialog GameBoyConfig from resources", this is
	    // probably the reason.
	    pn.Printf(wxT("cp%d"), i + 1);
	    wxWindow *w;
	    getfldv(w, pn, wxWindow);
	    GBColorConfigHandler[i].p = w;
	    GBColorConfigHandler[i].pno = i;
	    wxFarRadio *cb;
#define d w
	    getfld(cb, "UsePalette", wxFarRadio);
	    if(r)
		cb->SetGroup(r);
	    else
		r = cb;
	    cb->SetValidator(wxBoolIntValidator(&gbPaletteOption, i));
	    getfld(ch, "ColorSet", wxChoice);
	    GBColorConfigHandler[i].c = ch;
	    for(int j = 0; j < 8; j++) {
		wxString s;
		s.Printf(wxT("Color%d"), j);
		wxColourPickerCtrl *cp;
		getfldv(cp, s, wxColourPickerCtrl);
		GBColorConfigHandler[i].cp[j] = cp;
		cp->SetValidator(wxColorValidator(&systemGbPalette[i * 8 + j]));
	    }
	    w->Connect(wxEVT_COMMAND_CHOICE_SELECTED,
		       wxCommandEventHandler(GBColorConfig_t::ColorSel),
		       NULL, &GBColorConfigHandler[i]);
	    w->Connect(XRCID("Reset"), wxEVT_COMMAND_BUTTON_CLICKED,
		       wxCommandEventHandler(GBColorConfig_t::ColorReset),
		       NULL, &GBColorConfigHandler[i]);
	    w->Connect(wxID_ANY, wxEVT_COMMAND_COLOURPICKER_CHANGED,
		       wxCommandEventHandler(GBColorConfig_t::ColorButton),
		       NULL, &GBColorConfigHandler[i]);
#undef d
	}
    }

    LoadXRCDialog("GameBoyAdvanceConfig");
    {
	/// System and peripherals
	getch("SaveType", gopts.save_type);
	BatConfigHandler.type = ch;
	getch("FlashSize", gopts.flash_size);
	BatConfigHandler.size = ch;
	d->Connect(XRCID("SaveType"), wxEVT_COMMAND_CHOICE_SELECTED,
		   wxCommandEventHandler(BatConfig_t::ChangeType),
		   NULL, &BatConfigHandler);
#define getgbaw(n) do { \
    wxWindow *w = d->FindWindow(XRCID(n)); \
    if(!w) \
	baddialogc(n); \
    w->SetValidator(GBACtrlEnabler()); \
} while(0)
	getgbaw("Detect");
	d->Connect(XRCID("Detect"), wxEVT_COMMAND_BUTTON_CLICKED,
		   wxCommandEventHandler(BatConfig_t::Detect),
		   NULL, &BatConfigHandler);
	getcbb("RTC", gopts.rtc);
	getcbb("AGBPrinter", gopts.agbprint);

	/// Speed
	// AutoSkip/FrameSkip are 2 controls for 1 value.  Needs post-process
	// to ensure checkbox not ignored
	getcbie("FrameSkipAuto", gopts.gba_frameskip, -1);
	getsc("FrameSkip", gopts.gba_frameskip);
	addbier(sc, true);
	getlab("FrameSkipLab");
	addbier(lab, true);

	/// Boot ROM
	getcbbe("BootRomEn", gopts.gb_use_bios);
	getfp("BootRom", gopts.gb_bios);
	addbe(fp);
	getlab("BootRomLab");
	addbe(lab);
	getcbb("SkipIntro", gopts.skip_intro);
	addbe(cb);
	// doesn't work right now
	cb->Hide();

	/// Game Overrides
	getgbaw("GameSettings");
	// the rest must be filled in by command handler; just validate
	vfld("Comment", wxTextCtrl);
	vfld("OvRTC", wxChoice);
	vfld("OvSaveType", wxChoice);
	vfld("OvFlashSize", wxChoice);
	vfld("OvMirroring", wxChoice);
    }

    LoadXRCDialog("DisplayConfig");
    {
	/// On-Screen Display
	getch("SpeedIndicator", gopts.osd_speed);
	getcbb("NoStatusMsg", gopts.no_osd_status);
	getcbb("Transparent", gopts.osd_transparent);

	/// Zoom
	// this was a choice, but I'd rather not have to make an off-by-one
	// validator just for this, and spinctrl is good enough.
	getsc("DefaultScale", gopts.video_scale);
	getcbb("RetainAspect", gopts.retain_aspect);
	getsc("MaxScale", gopts.max_scale);
	// fs modes should be filled in at popup time
	// since they may change based on what screen is current
	vfld("FullscreenMode", wxChoice);
	getcbb("Fullscreen", gopts.fullscreen);

	/// Advanced
	getrbi("OutputSimple", gopts.render_method, RND_SIMPLE);
	getrbi("OutputOpenGL", gopts.render_method, RND_OPENGL);
#ifdef NO_OGL
	rb->Hide();
#endif
	getrbi("OutputCairo", gopts.render_method, RND_CAIRO);
#ifdef NO_CAIRO
	rb->Hide();
#endif
	getrbi("OutputDirect3D", gopts.render_method, RND_DIRECT3D);
#if !defined(__WXMSW__) || defined(NO_D3D) || 1 // not implemented
	rb->Hide();
#endif
	getcbb("Bilinear", gopts.bilinear);
	getcbb("VSync", gopts.vsync);
	// FIXME: make cb disabled when not GL or d3d
#define getcbi(n, o, v) do { \
    getfld(cb, n, wxCheckBox); \
    cb->SetValidator(wxBoolIntValidator(&o, v)); \
} while(0)
	int mthr = wxThread::GetCPUCount();
	if(mthr > 8)
	    mthr = 8;
	if(mthr < 0)
	    mthr = 2;
	getcbi("Multithread", gopts.max_threads, mthr);
	if(mthr <= 1)
	    cb->Hide();
#ifdef MMX
	getcbb("MMX", cpu_mmx);
#else
	getfld(cb, "MMX", wxCheckBox);
	cb->Hide();
#endif
	getch("Filter", gopts.filter);
	// these two are filled and/or hidden at dialog load time
	wxControl *pll;
	wxChoice *pl;
	getfld(pll, "PluginLab", wxControl);
	getfld(pl, "Plugin", wxChoice);
	pll->SetValidator(PluginEnabler());
	pl->SetValidator(PluginListFiller(d, pll, ch));
	PluginEnableHandler.lab = pll;
	PluginEnableHandler.ch = pl;
	ch->Connect(wxEVT_COMMAND_CHOICE_SELECTED,
		    wxCommandEventHandler(PluginEnable_t::ToggleChoice),
		    NULL, &PluginEnableHandler);
	getch("IFB", gopts.ifb);
    }

    LoadXRCDialog("SoundConfig");
    wxSlider *sl;
#define getsl(n, o) do { \
    getfld(sl, n, wxSlider); \
    sl->SetValidator(wxGenericValidator(&o)); \
} while(0)
    {
	/// Basic
	getsl("Volume", gopts.sound_vol);
	sound_config_handler.vol = sl;
	d->Connect(XRCID("Volume100"), wxEVT_COMMAND_BUTTON_CLICKED,
		   wxCommandEventHandler(SoundConfig_t::FullVol),
		   NULL, &sound_config_handler);
	getch("Rate", gopts.sound_qual);

	/// Advanced
#define audapi_rb(n, v) do {\
    getrbi(n, gopts.audio_api, v); \
    rb->Connect(wxEVT_COMMAND_RADIOBUTTON_SELECTED, \
		wxCommandEventHandler(SoundConfig_t::SetAPI), \
		NULL, &sound_config_handler); \
} while(0)
	audapi_rb("SDL", AUD_SDL);
	audapi_rb("OpenAL", AUD_OPENAL);
#ifdef NO_OAL
	rb->Hide();
#endif
	audapi_rb("DirectSound", AUD_DIRECTSOUND);
#ifndef __WXMSW__
	rb->Hide();
#endif
	audapi_rb("XAudio2", AUD_XAUDIO2);
#if !defined(__WXMSW__) || defined(NO_XAUDIO2)
	rb->Hide();
#endif
	getfld(sound_config_handler.dev, "Device", wxChoice);
	sound_config_handler.dev->SetValidator(SoundConfigLoad());
	getcbb("Upmix", gopts.upmix);
	sound_config_handler.umix = cb;
#if !defined(__WXMSW__) || defined(NO_XAUDIO2)
	cb->Hide();
#endif
	getcbb("HWAccel", gopts.dsound_hw_accel);
	sound_config_handler.hwacc = cb;
#ifndef __WXMSW__
	cb->Hide();
#endif
	getcbb("SyncGameAudio", synchronize);
	getsl("Buffers", gopts.audio_buffers);
	sound_config_handler.bufs = sl;
	getlab("BuffersInfo");
	sound_config_handler.bufinfo = lab;
	sl->Connect(wxEVT_SCROLL_CHANGED,
		    wxCommandEventHandler(SoundConfig_t::AdjustFramesEv),
		    NULL, &sound_config_handler);
	sl->Connect(wxEVT_SCROLL_THUMBTRACK,
		    wxCommandEventHandler(SoundConfig_t::AdjustFramesEv),
		    NULL, &sound_config_handler);
	sound_config_handler.AdjustFrames(10);

	/// Game Boy
	getcbb("GBDeclicking", gopts.gb_declick);
	getcbbe("GBEnhanceSound", gb_effects_config.enabled);
	wxPanel *p;
	getfld(p, "GBEnhanceSoundDep", wxPanel);
	addbe(p);
	getcbb("GBSurround", gb_effects_config.surround);
	getsl("GBEcho", gopts.gb_echo);
	getsl("GBStereo", gopts.gb_stereo);

	/// Game Boy Advance
	getcbb("GBASoundInterpolation", soundInterpolation);
	getsl("GBASoundFiltering", gopts.gba_sound_filter);
    }

    wxDirPickerCtrl *dp;
#define getdp(n, o) do { \
    getfld(dp, n, wxDirPickerCtrl); \
    dp->SetValidator(wxFileDirPickerValidator(&o)); \
} while(0)
    LoadXRCDialog("DirectoriesConfig");
    {
	getdp("GBARoms", gopts.gba_rom_dir);
	getdp("GBRoms", gopts.gb_rom_dir);
	getdp("BatSaves", gopts.battery_dir);
	getdp("StateSaves", gopts.state_dir);
	getdp("Screenshots", gopts.scrshot_dir);
	getdp("Recordings", gopts.recording_dir);
    }

    LoadXRCDialog("JoypadConfig");
    wxFarRadio *r = 0;
    for(int i = 0; i < 4; i++) {
	wxString pn;
	// NOTE: wx2.9.1 behaves differently for referenced nodes
	// than 2.8!  Unless there is an actual child node, the ID field
	// will not be overwritten.  This means that there should be a
	// dummy child node (e.g. position=(0,0)).  If you get
	// "Unable to load dialog JoypadConfig from resources", this is
	// probably the reason.
	pn.Printf(wxT("joy%d"), i + 1);
	wxWindow *w;
	getfldv(w, pn, wxWindow);
#define d w
	wxFarRadio *cb;
	getfld(cb, "DefaultConfig", wxFarRadio);
	if(r)
	    cb->SetGroup(r);
	else
	    r = cb;
	cb->SetValidator(wxBoolIntValidator(&gopts.default_stick, i + 1));
	wxWindow *prev = NULL, *prevp = NULL;
	for(int j = 0; j < NUM_KEYS; j++) {
	    wxJoyKeyTextCtrl *tc = XRCCTRL_D(*w, joynames[j], wxJoyKeyTextCtrl);
	    if(!tc)
		baddialogcv(joynames[j]);
	    wxWindow *p = tc->GetParent();
	    if(p == prevp)
		tc->MoveAfterInTabOrder(prev);
	    prev = tc;
	    prevp = p;
	    tc->SetValidator(wxJoyKeyValidator(&gopts.joykey_bindings[i][j]));
	}
	JoyPadConfigHandler[i].p = w;
	w->Connect(XRCID("Defaults"), wxEVT_COMMAND_BUTTON_CLICKED,
		   wxCommandEventHandler(JoyPadConfig_t::JoypadConfigButtons),
		   NULL, &JoyPadConfigHandler[i]);
	w->Connect(XRCID("Clear"), wxEVT_COMMAND_BUTTON_CLICKED,
		   wxCommandEventHandler(JoyPadConfig_t::JoypadConfigButtons),
		   NULL, &JoyPadConfigHandler[i]);
#undef d
    }

#ifndef NO_LINK
    LoadXRCDialog("LinkConfig");
    {
	getcbbe("Joybus", gba_joybus_enabled);
	getlab("JoybusHostLab");
	addbe(lab);
	gettc("JoybusHost", gopts.joybus_host);
	tc->SetValidator(IPHostValidator(&gopts.joybus_host));
	addbe(tc);
	getcbbe("Link", gba_link_enabled);
	getcbb("RFU", rfu_enabled);
	addbe(cb);
	getlab("LinkTimeoutLab");
	addbe(lab);
	getsc("LinkTimeout", linktimeout);
	addbe(sc);
    }
#endif

    LoadXRCDialog("AccelConfig");
    {
	wxTreeCtrl *tc;
	getfld(tc, "Commands", wxTreeCtrl);
	accel_config_handler.tc = tc;
	wxControlWithItems *lb;
	getfld(lb, "Current", wxControlWithItems);
	accel_config_handler.lb = lb;
	getfld(accel_config_handler.asb, "Assign", wxButton);
	getfld(accel_config_handler.remb, "Remove", wxButton);
	getfld(accel_config_handler.key, "Shortcut", wxKeyTextCtrl);
	getfld(accel_config_handler.curas, "AlreadyThere", wxControl);
	accel_config_handler.key->MoveBeforeInTabOrder(accel_config_handler.asb);
	accel_config_handler.key->SetMultikey(0);
	accel_config_handler.key->SetClearable(false);
	wxTreeItemId rid = tc->AddRoot(wxT("root"));
	if(menubar) {
	    wxTreeItemId mid = tc->AppendItem(rid, _("Menu commands"));
	    for(int i = 0; i < menubar->GetMenuCount(); i++) {
#if wxCHECK_VERSION(2,8,8)
		wxTreeItemId id = tc->AppendItem(mid, menubar->GetMenuLabelText(i));
#else
		// 2.8.4 has no equivalent for GetMenuLabelText()
		wxString txt = menubar->GetMenuLabel(i);
		txt.Replace(wxT("&"), wxT(""));
		wxTreeItemId id = tc->AppendItem(mid, txt);
#endif
		add_menu_accels(tc, id, menubar->GetMenu(i));
	    }
	}
	wxTreeItemId oid;
	int noop_id = XRCID("NOOP");
	for(int i = 0; i < ncmds; i++) {
	    if(cmdtab[i].mi || (recent && cmdtab[i].cmd_id >= wxID_FILE1 &&
			        cmdtab[i].cmd_id <= wxID_FILE10) ||
	       cmdtab[i].cmd_id == noop_id)
		continue;
	    if(!oid.IsOk())
		oid = tc->AppendItem(rid, _("Other commands"));
	    TreeInt *val = new TreeInt(i);
	    tc->AppendItem(oid, cmdtab[i].name, -1, -1, val);
	}
	tc->ExpandAll();
	// FIXME: make this actually show the entire line w/o scrolling
	// BestSize cuts off on rhs; MaxSize is completely invalid
	wxSize sz = tc->GetBestSize();
	if(sz.GetHeight() > 200)
	    sz.SetHeight(200);
	tc->SetSize(sz);
	sz.SetWidth(-1);  // maybe allow it to become bigger
	tc->SetSizeHints(sz, sz);
	int w, h;
	lb->GetTextExtent(wxT("CTRL-ALT-SHIFT-ENTER"), &w, &h);
	sz.Set(w, h);
	lb->SetMinSize(sz);
	sz.Set(0, 0);
	wxControl *curas = accel_config_handler.curas;
	for(int i = 0; i < ncmds; i++) {
	    wxString labs;
	    treeid_to_name(i, labs, tc, tc->GetRootItem());
	    curas->GetTextExtent(labs, &w, &h);
	    if(w > sz.GetWidth())
		sz.SetWidth(w);
	    if(h > sz.GetHeight())
		sz.SetHeight(h);
	}
	curas->SetSize(sz);
	curas->SetSizeHints(sz);
	tc->Connect(wxEVT_COMMAND_TREE_SEL_CHANGING,
		   wxTreeEventHandler(AccelConfig_t::CommandSel),
		   NULL, &accel_config_handler);
	tc->Connect(wxEVT_COMMAND_TREE_SEL_CHANGED,
		   wxTreeEventHandler(AccelConfig_t::CommandSel),
		   NULL, &accel_config_handler);
	d->Connect(wxEVT_SHOW, wxShowEventHandler(AccelConfig_t::Init),
		   NULL, &accel_config_handler);
	d->Connect(wxID_OK, wxEVT_COMMAND_BUTTON_CLICKED,
		   wxCommandEventHandler(AccelConfig_t::Set),
		   NULL, &accel_config_handler);
	d->Connect(XRCID("Assign"), wxEVT_COMMAND_BUTTON_CLICKED,
		   wxCommandEventHandler(AccelConfig_t::Assign),
		   NULL, &accel_config_handler);
	d->Connect(XRCID("Remove"), wxEVT_COMMAND_BUTTON_CLICKED,
		   wxCommandEventHandler(AccelConfig_t::Remove),
		   NULL, &accel_config_handler);
	d->Connect(XRCID("ResetAll"), wxEVT_COMMAND_BUTTON_CLICKED,
		   wxCommandEventHandler(AccelConfig_t::ResetAll),
		   NULL, &accel_config_handler);
	lb->Connect(wxEVT_COMMAND_LISTBOX_SELECTED,
		    wxCommandEventHandler(AccelConfig_t::KeySel),
		    NULL, &accel_config_handler);
	d->Connect(XRCID("Shortcut"), wxEVT_COMMAND_TEXT_UPDATED,
		   wxCommandEventHandler(AccelConfig_t::CheckKey),
		   NULL, &accel_config_handler);
    }

    d->Fit();

    //// Debug menu
    // actually, the viewers can be instantiated multiple times.
    // since they're for debugging, it's probably OK to just detect errors
    // at popup time.
    // The only one that can only be popped up once is logging, so allocate
    // and check it already.
    logdlg = new LogDialog;

    // activate OnDropFile event handler
#if !defined(__WXGTK__) || wxCHECK_VERSION(2,8,10)
    // may not actually do anything, but verfied to work w/ Linux/Nautilus
    DragAcceptFiles(true);
#endif

    // delayed fullscreen
    if(wxGetApp().pending_fullscreen || gopts.fullscreen)
	panel->ShowFullScreen(true);

#ifndef NO_LINK
    if(gba_joybus_enabled) {
	bool isv = !gopts.joybus_host.empty();
	if(isv) {
	    joybusHostAddr = std::string(gopts.joybus_host.mb_str());
	    isv = joybusHostAddr.IsValid();
	}
	if(!isv) {
	    wxLogError(_("JoyBus host invalid; disabling"));
	    gba_joybus_enabled = false;
	} else
	    JoyBusConnect();
    }
    if(gba_link_enabled)
	if((did_link_init = InitLink()))
	    cmd_enable |= CMDEN_LINK_ANY;

#endif
    panel->SetFrameTitle();

    // All OK; activate idle loop
    panel->SetExtraStyle(panel->GetExtraStyle() | wxWS_EX_PROCESS_IDLE);

    return true;
}


wxAcceleratorEntry_v MainFrame::get_accels(wxAcceleratorEntry_v user_accels)
{
    // set global accelerators
    // first system
    wxAcceleratorEntry_v accels = sys_accels;
    // then user overrides
    // silently keep only last defined binding
    // same horribly inefficent O(n*m) search for duplicates as above..
    for(int i = 0; i < user_accels.size(); i++) {
	const wxAcceleratorEntry &ae = user_accels[i];
	for(wxAcceleratorEntry_v::iterator e = accels.begin(); e < accels.end(); e++)
	    if(ae.GetFlags() == e->GetFlags() &&
	       ae.GetKeyCode() == e->GetKeyCode()) {
		accels.erase(e);
		break;
	    }
	accels.push_back(ae);
    }
    return accels;
}

void MainFrame::set_global_accels()
{
    wxAcceleratorEntry_v accels = get_accels(gopts.accels);

    // this is needed on Wine/win32 to support accels for close & quit
    wxGetApp().accels = accels;

    // Update menus; this probably takes the longest
    // as a side effect, any system-defined accels that weren't already in
    // the menus will be added now

    // first, zero out menu item on all accels
    for(int i = 0; i < accels.size(); i++)
	accels[i].Set(accels[i].GetFlags(), accels[i].GetKeyCode(),
		      accels[i].GetCommand());

    // yet another O(n*m) loop.  I really ought to sort the accel arrays
    for(int i = 0; i < ncmds; i++) {
	wxMenuItem *mi = cmdtab[i].mi;
	if(!mi)
	    continue;
	// only *last* accelerator is made visible in menu
	// and is flagged as such by setting menu item in accel
	// the last is chosen so menu overrides non-menu and user overrides
	// system
	int cmd = cmdtab[i].cmd_id;
	int last_accel = -1;
	for(int j = 0; j < accels.size(); j++)
	    if(cmd == accels[j].GetCommand())
		last_accel = j;
	if(last_accel >= 0) {
	    DoSetAccel(mi, &accels[last_accel]);
	    accels[last_accel].Set(accels[last_accel].GetFlags(),
				   accels[last_accel].GetKeyCode(),
				   accels[last_accel].GetCommand(), mi);
	} else
	    // clear out user-cleared menu items
	    DoSetAccel(mi, NULL);
    }
    // Finally, install a global accelerator table for any non-menu accels
    int len = 0;
    for(int i = 0; i < accels.size(); i++)
	if(!accels[i].GetMenuItem())
	    len++;
    if(len) {
	wxAcceleratorEntry tab[len];
	for(int i = 0, j = 0; i < accels.size(); i++)
	    if(!accels[i].GetMenuItem())
		tab[j++] = accels[i];
	wxAcceleratorTable atab(len, tab);
	// set the table on the panel, where focus usually is
	// otherwise accelerators are lost sometimes
	panel->SetAcceleratorTable(atab);
    } else
	panel->SetAcceleratorTable(wxNullAcceleratorTable);

    // save recent accels
    for(int i = 0; i < 10; i++)
	recent_accel[i] = wxAcceleratorEntry();
    for(int i = 0; i < accels.size(); i++)
	if(accels[i].GetCommand() >= wxID_FILE1 &&
	   accels[i].GetCommand() <= wxID_FILE10)
	    recent_accel[accels[i].GetCommand() - wxID_FILE1] = accels[i];
    SetRecentAccels();
}
