wxwidgets: exit-fullscreen to forcibly exit fullscreen mode
[lsnes.git] / src / platform / wxwidgets / mainwindow.cpp
blobbb252bb77abb1119ecfa54a397cd510007218bdb
1 #include "lsnes.hpp"
3 #include <wx/dnd.h>
4 #include "platform/wxwidgets/menu_dump.hpp"
5 #include "platform/wxwidgets/menu_upload.hpp"
6 #include "platform/wxwidgets/platform.hpp"
7 #include "platform/wxwidgets/loadsave.hpp"
8 #include "platform/wxwidgets/window_mainwindow.hpp"
9 #include "platform/wxwidgets/window_messages.hpp"
10 #include "platform/wxwidgets/window_status.hpp"
11 #include "platform/wxwidgets/window-romload.hpp"
12 #include "platform/wxwidgets/settings-common.hpp"
13 #include "platform/wxwidgets/menu_tracelog.hpp"
14 #include "platform/wxwidgets/menu_branches.hpp"
15 #include "platform/wxwidgets/menu_projects.hpp"
17 #include "cmdhelp/framebuffer.hpp"
18 #include "cmdhelp/loadsave.hpp"
19 #include "cmdhelp/lua.hpp"
20 #include "cmdhelp/subtitles.hpp"
21 #include "core/audioapi.hpp"
22 #include "core/audioapi-driver.hpp"
23 #include "core/command.hpp"
24 #include "core/controller.hpp"
25 #include "core/controllerframe.hpp"
26 #include "core/dispatch.hpp"
27 #include "core/emustatus.hpp"
28 #include "core/framebuffer.hpp"
29 #include "core/framerate.hpp"
30 #include "core/instance.hpp"
31 #include "core/keymapper.hpp"
32 #include "core/ui-services.hpp"
33 #include "interface/romtype.hpp"
34 #include "core/loadlib.hpp"
35 #include "lua/lua.hpp"
36 #include "core/mainloop.hpp"
37 #include "core/memorywatch.hpp"
38 #include "core/messages.hpp"
39 #include "core/misc.hpp"
40 #include "core/moviedata.hpp"
41 #include "core/project.hpp"
42 #include "core/rom.hpp"
43 #include "core/settings.hpp"
44 #include "core/window.hpp"
45 #include "library/directory.hpp"
46 #include "library/minmax.hpp"
47 #include "library/string.hpp"
48 #include "library/zip.hpp"
49 #if defined(_WIN32) || defined(_WIN64) || defined(TEST_WIN32_CODE)
50 #define FUCKED_SYSTEM
51 #endif
53 #include <cmath>
54 #include <vector>
55 #include <string>
58 extern "C"
60 #ifndef UINT64_C
61 #define UINT64_C(val) val##ULL
62 #endif
63 #include <libswscale/swscale.h>
66 enum
68 wxID_PAUSE = wxID_HIGHEST + 1,
69 wxID_FRAMEADVANCE,
70 wxID_SUBFRAMEADVANCE,
71 wxID_NEXTPOLL,
72 wxID_AUDIO_ENABLED,
73 wxID_SAVE_STATE,
74 wxID_SAVE_MOVIE,
75 wxID_SAVE_SUBTITLES,
76 wxID_LOAD_STATE,
77 wxID_LOAD_MOVIE,
78 wxID_RUN_SCRIPT,
79 wxID_RUN_LUA,
80 wxID_RESET_LUA,
81 wxID_EVAL_LUA,
82 wxID_SAVE_SCREENSHOT,
83 wxID_READONLY_MODE,
84 wxID_EDIT_AUTHORS,
85 wxID_AUTOHOLD,
86 wxID_EDIT_MEMORYWATCH,
87 wxID_SAVE_MEMORYWATCH,
88 wxID_LOAD_MEMORYWATCH,
89 wxID_EDIT_SUBTITLES,
90 wxID_EDIT_VSUBTITLES,
91 wxID_DUMP_FIRST,
92 wxID_DUMP_LAST = wxID_DUMP_FIRST + 1023,
93 wxID_REWIND_MOVIE,
94 wxID_MEMORY_SEARCH,
95 wxID_CANCEL_SAVES,
96 wxID_SHOW_STATUS,
97 wxID_SET_SPEED,
98 wxID_SPEED_5,
99 wxID_SPEED_10,
100 wxID_SPEED_17,
101 wxID_SPEED_20,
102 wxID_SPEED_25,
103 wxID_SPEED_33,
104 wxID_SPEED_50,
105 wxID_SPEED_100,
106 wxID_SPEED_150,
107 wxID_SPEED_200,
108 wxID_SPEED_300,
109 wxID_SPEED_500,
110 wxID_SPEED_1000,
111 wxID_SPEED_TURBO,
112 wxID_LOAD_LIBRARY,
113 wxID_RELOAD_ROM_IMAGE,
114 wxID_LOAD_ROM_IMAGE_FIRST,
115 wxID_LOAD_ROM_IMAGE_LAST = wxID_LOAD_ROM_IMAGE_FIRST + 1023,
116 wxID_NEW_MOVIE,
117 wxID_SHOW_MESSAGES,
118 wxID_DEDICATED_MEMORY_WATCH,
119 wxID_RMOVIE_FIRST,
120 wxID_RMOVIE_LAST = wxID_RMOVIE_FIRST + 16,
121 wxID_RROM_FIRST,
122 wxID_RROM_LAST = wxID_RROM_FIRST + 16,
123 wxID_CONFLICTRESOLUTION,
124 wxID_VUDISPLAY,
125 wxID_MOVIE_EDIT,
126 wxID_TASINPUT,
127 wxID_NEW_PROJECT,
128 wxID_CLOSE_PROJECT,
129 wxID_CLOSE_ROM,
130 wxID_EDIT_MACROS,
131 wxID_ENTER_FULLSCREEN,
132 wxID_ACTIONS_FIRST,
133 wxID_ACTIONS_LAST = wxID_ACTIONS_FIRST + 256,
134 wxID_SETTINGS_FIRST,
135 wxID_SETTINGS_LAST = wxID_SETTINGS_FIRST + 256,
136 wxID_HEXEDITOR,
137 wxID_MULTITRACK,
138 wxID_CHDIR,
139 wxID_RLUA_FIRST,
140 wxID_RLUA_LAST = wxID_RLUA_FIRST + 16,
141 wxID_UPLOAD_FIRST,
142 wxID_UPLOAD_LAST = wxID_UPLOAD_FIRST + 256,
143 wxID_DOWNLOAD,
144 wxID_TRACELOG_FIRST,
145 wxID_TRACELOG_LAST = wxID_TRACELOG_FIRST + 256,
146 wxID_PLUGIN_MANAGER,
147 wxID_BRANCH_FIRST,
148 wxID_BRANCH_LAST = wxID_BRANCH_FIRST + 10240,
149 wxID_PROJECT_FIRST,
150 wxID_PROJECT_LAST = wxID_PROJECT_FIRST + 17,
151 wxID_DISASSEMBLER,
155 double video_scale_factor = 1.0;
156 int scaling_flags = SWS_POINT;
157 bool arcorrect_enabled = false;
158 bool hflip_enabled = false;
159 bool vflip_enabled = false;
160 bool rotate_enabled = false;
162 namespace
164 std::string last_volume = "0dB";
165 std::string last_volume_record = "0dB";
166 std::string last_volume_voice = "0dB";
167 unsigned char* screen_buffer;
168 struct SwsContext* sws_ctx;
169 uint32_t* rotate_buffer;
170 uint32_t old_width;
171 uint32_t old_height;
172 int old_flags = SWS_POINT;
173 bool old_hflip = false;
174 bool old_vflip = false;
175 bool old_rotate = false;
176 bool main_window_dirty;
177 bool is_fs = false;
178 bool hashing_in_progress = false;
179 uint64_t hashing_left = 0;
180 uint64_t hashing_total = 0;
181 int64_t last_update = 0;
182 threads::thread* emulation_thread;
183 bool status_updated = false;
184 bool becoming_fullscreen = false;
186 settingvar::variable<settingvar::model_bool<settingvar::yes_no>> background_audio(*lsnes_instance.settings,
187 "background-audio", "GUIā€£Enable background audio", true);
189 class _status_timer : public wxTimer
191 public:
192 _status_timer()
194 Start(50);
196 void Notify()
198 if(status_updated) {
199 status_updated = false;
200 if(main_window) main_window->update_statusbar();
205 void cleanup_dead_download_timers();
206 class _focus_timer : public wxTimer
208 public:
209 _focus_timer()
211 was_focused = (wxWindow::FindFocus() != NULL);
212 was_enabled = platform::is_sound_enabled();
213 Start(500);
215 void Notify()
217 CHECK_UI_THREAD;
218 bool is_focused = (wxWindow::FindFocus() != NULL);
219 if(is_focused && !was_focused) {
220 //Gained focus.
221 if(!background_audio)
222 platform::sound_enable(was_enabled);
223 } else if(!is_focused && was_focused) {
224 //Lost focus.
225 was_enabled = platform::is_sound_enabled();
226 if(!background_audio)
227 platform::sound_enable(false);
229 was_focused = is_focused;
230 cleanup_dead_download_timers();
232 private:
233 bool was_focused;
234 bool was_enabled;
237 class download_timer;
238 std::list<download_timer*> download_timer_gc_queue;
240 class download_timer : public wxTimer
242 public:
243 download_timer(wxwin_mainwindow* main, emulator_instance& _inst)
244 : inst(_inst)
246 w = main;
247 Start(50);
249 void Notify()
251 CHECK_UI_THREAD;
252 if(!w->download_in_progress) {
253 //Received a call with download finish already done. Ignore.
254 return;
256 if(w->download_in_progress->finished) {
257 w->update_statusbar();
258 auto old = w->download_in_progress;
259 w->download_in_progress = NULL;
260 if(old->errormsg != "") {
261 show_message_ok(w, "Error downloading movie", old->errormsg,
262 wxICON_EXCLAMATION);
263 } else {
264 inst.iqueue->queue(CLOADSAVE::ldm.name, "$MEMORY:wxwidgets_download_tmp");
266 delete old;
267 try { download_timer_gc_queue.push_back(this); } catch(...) { Stop(); }
268 } else {
269 w->update_statusbar();
272 private:
273 emulator_instance& inst;
274 wxwin_mainwindow* w;
277 void cleanup_dead_download_timers()
279 for(auto i : download_timer_gc_queue) {
280 i->Stop();
281 delete i;
283 download_timer_gc_queue.clear();
286 void hash_callback(uint64_t left, uint64_t total)
288 wxwin_mainwindow* mwin = main_window;
289 if(left == 0xFFFFFFFFFFFFFFFFULL) {
290 hashing_in_progress = false;
291 runuifun([mwin]() { if(mwin) mwin->notify_update_status(); });
292 last_update = framerate_regulator::get_utime() - 2000000;
293 return;
295 hashing_in_progress = true;
296 hashing_left = left;
297 hashing_total = total;
298 int64_t this_update = framerate_regulator::get_utime();
299 if(this_update < last_update - 1000000 || this_update > last_update + 1000000) {
300 runuifun([mwin]() { if(mwin) mwin->notify_update_status(); });
301 last_update = this_update;
305 std::pair<std::string, std::string> lsplit(std::string l)
307 for(unsigned i = 0; i < l.length() - 3; i++)
308 if((uint8_t)l[i] == 0xE2 && (uint8_t)l[i + 1] == 0x80 && (uint8_t)l[i + 2] == 0xA3)
309 return std::make_pair(l.substr(0, i), l.substr(i + 3));
310 return std::make_pair("", l);
313 recentfiles::multirom loadreq_to_multirom(const romload_request& req)
315 recentfiles::multirom r;
316 r.packfile = req.packfile;
317 r.singlefile = req.singlefile;
318 r.core = req.core;
319 r.system = req.system;
320 r.region = req.region;
321 for(unsigned i = 0; i < ROM_SLOT_COUNT; i++)
322 if(req.files[i] != "") {
323 r.files.resize(i + 1);
324 r.files[i] = req.files[i];
326 return r;
329 class system_menu : public wxMenu
331 public:
332 system_menu(wxWindow* win, emulator_instance& _inst);
333 ~system_menu();
334 void on_select(wxCommandEvent& e);
335 void update(bool light);
336 private:
337 emulator_instance& inst;
338 wxWindow* pwin;
339 void insert_pass(int id, const std::string& label);
340 void insert_act(unsigned id, const std::string& label, bool dots, bool check);
341 wxMenu* lookup_menu(const std::string& key);
342 wxMenuItem* sep;
343 std::map<int, unsigned> action_by_id;
344 std::map<unsigned, wxMenuItem*> item_by_action;
345 std::map<wxMenuItem*, wxMenu*> menu_by_item;
346 std::map<std::string, wxMenu*> submenu_by_name;
347 std::map<std::string, wxMenuItem*> submenui_by_name;
348 std::set<unsigned> toggles;
349 int next_id;
352 wxMenu* system_menu::lookup_menu(const std::string& key)
354 CHECK_UI_THREAD;
355 if(key == "")
356 return this;
357 if(submenu_by_name.count(key))
358 return submenu_by_name[key];
359 //Not found, create.
360 if(!sep)
361 sep = AppendSeparator();
362 auto p = lsplit(key);
363 wxMenu* into = lookup_menu(p.first);
364 submenu_by_name[key] = new wxMenu();
365 submenui_by_name[key] = into->AppendSubMenu(submenu_by_name[key], towxstring(p.second));
366 menu_by_item[submenui_by_name[key]] = into;
367 return submenu_by_name[key];
370 void system_menu::insert_act(unsigned id, const std::string& label, bool dots, bool check)
372 CHECK_UI_THREAD;
373 if(!sep)
374 sep = AppendSeparator();
376 auto p = lsplit(label);
377 wxMenu* into = lookup_menu(p.first);
379 action_by_id[next_id] = id;
380 std::string use_label = p.second + (dots ? "..." : "");
381 if(check) {
382 item_by_action[id] = into->AppendCheckItem(next_id, towxstring(use_label));
383 toggles.insert(id);
384 } else
385 item_by_action[id] = into->Append(next_id, towxstring(use_label));
386 menu_by_item[item_by_action[id]] = into;
387 pwin->Connect(next_id++, wxEVT_COMMAND_MENU_SELECTED,
388 wxCommandEventHandler(system_menu::on_select), NULL, this);
391 void system_menu::insert_pass(int id, const std::string& label)
393 CHECK_UI_THREAD;
394 pwin->Connect(id, wxEVT_COMMAND_MENU_SELECTED,
395 wxCommandEventHandler(wxwin_mainwindow::handle_menu_click), NULL, pwin);
396 Append(id, towxstring(label));
399 system_menu::system_menu(wxWindow* win, emulator_instance& _inst)
400 : inst(_inst)
402 CHECK_UI_THREAD;
403 pwin = win;
404 insert_pass(wxID_PAUSE, "Pause/Unpause");
405 insert_pass(wxID_FRAMEADVANCE, "Step frame");
406 insert_pass(wxID_SUBFRAMEADVANCE, "Step subframe");
407 insert_pass(wxID_NEXTPOLL, "Step poll");
408 sep = NULL;
411 system_menu::~system_menu()
415 void system_menu::on_select(wxCommandEvent& e)
417 CHECK_UI_THREAD;
418 if(!action_by_id.count(e.GetId()))
419 return;
420 unsigned act_id = action_by_id[e.GetId()];
421 const interface_action* act = NULL;
422 for(auto i : inst.rom->get_actions())
423 if(i->id == act_id) {
424 act = i;
425 break;
427 if(!act)
428 return;
429 try {
430 auto p = prompt_action_params(pwin, act->get_title(), act->params);
431 inst.iqueue->run([this, act_id,p]() { this->inst.rom->execute_action(act_id, p); });
432 } catch(canceled_exception& e) {
433 } catch(std::bad_alloc& e) {
434 OOM_panic();
438 void system_menu::update(bool light)
440 CHECK_UI_THREAD;
441 if(!light) {
442 next_id = wxID_ACTIONS_FIRST;
443 if(sep) {
444 Destroy(sep);
445 sep = NULL;
447 for(auto i = item_by_action.begin(); i != item_by_action.end(); i++)
448 menu_by_item[i->second]->Destroy(i->second);
449 for(auto i = submenui_by_name.rbegin(); i != submenui_by_name.rend(); i++)
450 menu_by_item[i->second]->Destroy(i->second);
451 action_by_id.clear();
452 item_by_action.clear();
453 menu_by_item.clear();
454 submenu_by_name.clear();
455 submenui_by_name.clear();
456 toggles.clear();
458 for(auto i : inst.rom->get_actions())
459 insert_act(i->id, i->get_title(), !i->params.empty(), i->is_toggle());
461 for(auto i : item_by_action)
462 i.second->Enable(inst.rom->action_flags(i.first) & 1);
463 for(auto i : toggles)
464 item_by_action[i]->Check(inst.rom->action_flags(i) & 2);
467 std::string munge_name(const std::string& orig)
469 std::string newname;
470 regex_results r;
471 if(r = regex("(.*)\\(([0-9]+)\\)", newname)) {
472 uint64_t sequence;
473 try {
474 sequence = parse_value<uint64_t>(r[2]);
475 newname = (stringfmt() << r[1] << "(" << sequence + 1 << ")").str();
476 } catch(...) {
477 newname = newname + "(2)";
479 } else {
480 newname = newname + "(2)";
482 return newname;
485 void handle_watch_load(emulator_instance& inst, std::map<std::string, std::string>& new_watches,
486 std::set<std::string>& old_watches)
488 auto proj = inst.project->get();
489 if(proj) {
490 for(auto i : new_watches) {
491 std::string name = i.first;
492 while(true) {
493 if(!old_watches.count(name)) {
494 try {
495 if(name != "" && i.second != "")
496 inst.mwatch->set(name, i.second);
497 } catch(std::exception& e) {
498 messages << "Can't set memory watch '" << name << "': "
499 << e.what() << std::endl;
501 break;
502 } else if(inst.mwatch->get_string(name) == i.second)
503 break;
504 else
505 name = munge_name(name);
508 } else {
509 for(auto i : new_watches)
510 try {
511 if(i.first != "" && i.second != "")
512 inst.mwatch->set(i.first, i.second);
513 } catch(std::exception& e) {
514 messages << "Can't set memory watch '" << i.first << "': "
515 << e.what() << std::endl;
517 for(auto i : old_watches)
518 if(!new_watches.count(i))
519 try {
520 inst.mwatch->clear(i);
521 } catch(std::exception& e) {
522 messages << "Can't clear memory watch '" << i << "': "
523 << e.what() << std::endl;
528 std::string get_default_screenshot_name(emulator_instance& inst)
530 auto p = inst.project->get();
531 if(!p)
532 return "";
533 else {
534 auto files = directory::enumerate(p->directory, ".*-[0-9]+\\.png");
535 std::set<std::string> numbers;
536 for(auto i : files) {
537 size_t split;
538 #ifdef FUCKED_SYSTEM
539 split = i.find_last_of("\\/");
540 #else
541 split = i.find_last_of("/");
542 #endif
543 std::string name = i;
544 if(split < name.length())
545 name = name.substr(split + 1);
546 regex_results r = regex("(.*)-([0-9]+)\\.png", name);
547 if(r[1] != p->prefix)
548 continue;
549 numbers.insert(r[2]);
551 for(uint64_t i = 1;; i++) {
552 std::string candidate = (stringfmt() << i).str();
553 if(!numbers.count(candidate))
554 return p->prefix + "-" + candidate + ".png";
559 std::string project_prefixname(emulator_instance& inst, const std::string ext)
561 auto p = inst.project->get();
562 if(!p)
563 return "";
564 else
565 return p->prefix + "." + ext;
569 void recent_rom_selected(emulator_instance& inst, const recentfiles::multirom& file)
571 romload_request req;
572 req.packfile = file.packfile;
573 req.singlefile = file.singlefile;
574 req.core = file.core;
575 req.system = file.system;
576 req.region = file.region;
577 for(unsigned i = 0; i < file.files.size() && i < ROM_SLOT_COUNT; i++)
578 req.files[i] = file.files[i];
579 inst.iqueue->run_async([req]() {
580 CORE().command->invoke("unpause-emulator");
581 load_new_rom(req);
582 }, [](std::exception& e) {});
585 void recent_movie_selected(emulator_instance& inst, const recentfiles::path& file)
587 inst.iqueue->queue(CLOADSAVE::ldsm.name, file.get_path());
590 void recent_script_selected(emulator_instance& inst, const recentfiles::path& file)
592 inst.iqueue->queue(CLUA::run.name, file.get_path());
595 wxString getname(emulator_instance& inst)
597 std::string windowname = "lsnes rr" + lsnes_version + " [";
598 auto p = inst.project->get();
599 if(p)
600 windowname = windowname + p->name;
601 else
602 windowname = windowname + inst.rom->get_core_identifier();
603 windowname = windowname + "]";
604 return towxstring(windowname);
607 struct emu_args
609 emulator_instance* inst;
610 struct loaded_rom rom;
611 struct moviefile* initial;
612 bool load_has_to_succeed;
615 void* emulator_main(void* _args)
617 struct emu_args* args = reinterpret_cast<struct emu_args*>(_args);
618 auto& inst = *args->inst;
619 try {
620 *inst.rom = args->rom;
621 messages << "Using core: " << inst.rom->get_core_identifier() << std::endl;
622 struct moviefile* movie = args->initial;
623 bool has_to_succeed = args->load_has_to_succeed;
624 platform::flush_command_queue();
625 main_loop(*inst.rom, *movie, has_to_succeed);
626 signal_program_exit();
627 } catch(std::bad_alloc& e) {
628 OOM_panic();
629 } catch(std::exception& e) {
630 messages << "FATAL: " << e.what() << std::endl;
631 platform::fatal_error();
633 delete args;
634 return NULL;
637 void join_emulator_thread()
639 emulation_thread->join();
642 bool is_readonly_mode(emulator_instance& inst)
644 bool ret;
645 inst.iqueue->run([&ret]() {
646 ret = *CORE().mlogic ? CORE().mlogic->get_movie().readonly_mode() : false;
648 return ret;
651 void set_speed(emulator_instance& inst, double target)
653 if(target < 0)
654 inst.framerate->set_speed_multiplier(std::numeric_limits<double>::infinity());
655 else
656 inst.framerate->set_speed_multiplier(target / 100);
659 void update_preferences()
661 preferred_core.clear();
662 for(auto i : core_type::get_core_types()) {
663 std::string val = i->get_hname() + " / " + i->get_core_identifier();
664 for(auto j : i->get_extensions()) {
665 std::string key = "ext:" + j;
666 if(core_selections.count(key) && core_selections[key] == val)
667 preferred_core[key] = i;
669 std::string key2 = "type:" + i->get_iname();
670 if(core_selections.count(key2) && core_selections[key2] == val)
671 preferred_core[key2] = i;
675 bool is_lsnes_movie(const std::string& filename)
677 std::istream* s = NULL;
678 try {
679 bool ans = false;
680 s = &zip::openrel(filename, "");
681 char buf[6] = {0};
682 s->read(buf, 5);
683 if(*s && !strcmp(buf, "lsmv\x1A"))
684 ans = true;
685 delete s;
686 if(ans) return true;
687 } catch(...) {
688 delete s;
690 try {
691 zip::reader r(filename);
692 std::istream& s = r["systemid"];
693 std::string s2;
694 std::getline(s, s2);
695 delete &s;
696 istrip_CR(s2);
697 return (s2 == "lsnes-rr1");
698 } catch(...) {
699 return false;
703 class loadfile : public wxFileDropTarget
705 public:
706 loadfile(wxwin_mainwindow* win, emulator_instance& _inst) : inst(_inst), pwin(win) {};
707 bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& filenames)
709 CHECK_UI_THREAD;
710 bool ret = false;
711 if(filenames.Count() == 2) {
712 std::string a = tostdstring(filenames[0]);
713 std::string b = tostdstring(filenames[1]);
714 bool amov = is_lsnes_movie(a);
715 bool bmov = is_lsnes_movie(b);
716 if(amov == bmov)
717 return false;
718 if(amov) std::swap(a, b);
719 inst.iqueue->run_async([a, b]() {
720 CORE().command->invoke("unpause-emulator");
721 romload_request req;
722 req.packfile = a;
723 load_new_rom(req);
724 CORE().command->invoke(CLOADSAVE::ldsm.name, b);
725 }, [](std::exception& e) {});
726 ret = true;
728 if(filenames.Count() == 1) {
729 std::string a = tostdstring(filenames[0]);
730 bool amov = is_lsnes_movie(a);
731 if(amov) {
732 inst.iqueue->queue(CLOADSAVE::ldsm.name, a);
733 pwin->recent_movies->add(a);
734 ret = true;
735 } else {
736 romload_request req;
737 req.packfile = a;
738 inst.iqueue->run_async([req]() {
739 CORE().command->invoke("unpause-emulator");
740 load_new_rom(req);
741 }, [](std::exception& e) {});
742 pwin->recent_roms->add(loadreq_to_multirom(req));
743 ret = true;
746 return ret;
748 emulator_instance& inst;
749 wxwin_mainwindow* pwin;
753 void boot_emulator(emulator_instance& inst, loaded_rom& rom, moviefile& movie, bool fscreen)
755 CHECK_UI_THREAD;
756 update_preferences();
757 try {
758 struct emu_args* a = new emu_args;
759 a->rom = rom;
760 a->initial = &movie;
761 a->load_has_to_succeed = false;
762 a->inst = &inst;
763 modal_pause_holder hld;
764 emulation_thread = new threads::thread(emulator_main, a);
765 main_window = new wxwin_mainwindow(inst, fscreen);
766 main_window->Show();
767 } catch(std::bad_alloc& e) {
768 OOM_panic();
772 wxwin_mainwindow::panel::panel(wxWindow* win, emulator_instance& _inst)
773 : wxPanel(win, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS), inst(_inst)
775 CHECK_UI_THREAD;
776 this->Connect(wxEVT_PAINT, wxPaintEventHandler(panel::on_paint), NULL, this);
777 this->Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(panel::on_erase), NULL, this);
778 this->Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(panel::on_keyboard_down), NULL, this);
779 this->Connect(wxEVT_KEY_UP, wxKeyEventHandler(panel::on_keyboard_up), NULL, this);
780 this->Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(panel::on_mouse), NULL, this);
781 this->Connect(wxEVT_LEFT_UP, wxMouseEventHandler(panel::on_mouse), NULL, this);
782 this->Connect(wxEVT_MIDDLE_DOWN, wxMouseEventHandler(panel::on_mouse), NULL, this);
783 this->Connect(wxEVT_MIDDLE_UP, wxMouseEventHandler(panel::on_mouse), NULL, this);
784 this->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(panel::on_mouse), NULL, this);
785 this->Connect(wxEVT_RIGHT_UP, wxMouseEventHandler(panel::on_mouse), NULL, this);
786 this->Connect(wxEVT_MOTION, wxMouseEventHandler(panel::on_mouse), NULL, this);
787 this->Connect(wxEVT_ENTER_WINDOW, wxMouseEventHandler(panel::on_mouse), NULL, this);
788 this->Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler(panel::on_mouse), NULL, this);
789 SetMinSize(wxSize(512, 448));
792 void wxwin_mainwindow::menu_start(wxString name)
794 CHECK_UI_THREAD;
795 while(!upper.empty())
796 upper.pop();
797 current_menu = new wxMenu();
798 menubar->Append(current_menu, name);
801 void wxwin_mainwindow::menu_special(wxString name, wxMenu* menu)
803 CHECK_UI_THREAD;
804 while(!upper.empty())
805 upper.pop();
806 menubar->Append(menu, name);
807 current_menu = NULL;
810 wxMenuItem* wxwin_mainwindow::menu_special_sub(wxString name, wxMenu* menu)
812 CHECK_UI_THREAD;
813 return current_menu->AppendSubMenu(menu, name);
816 void wxwin_mainwindow::menu_entry(int id, wxString name)
818 CHECK_UI_THREAD;
819 current_menu->Append(id, name);
820 Connect(id, wxEVT_COMMAND_MENU_SELECTED,
821 wxCommandEventHandler(wxwin_mainwindow::wxwin_mainwindow::handle_menu_click), NULL, this);
824 void wxwin_mainwindow::menu_entry_check(int id, wxString name)
826 CHECK_UI_THREAD;
827 checkitems[id] = current_menu->AppendCheckItem(id, name);
828 Connect(id, wxEVT_COMMAND_MENU_SELECTED,
829 wxCommandEventHandler(wxwin_mainwindow::wxwin_mainwindow::handle_menu_click), NULL, this);
832 void wxwin_mainwindow::menu_start_sub(wxString name)
834 CHECK_UI_THREAD;
835 wxMenu* old = current_menu;
836 upper.push(current_menu);
837 current_menu = new wxMenu();
838 old->AppendSubMenu(current_menu, name);
841 void wxwin_mainwindow::menu_end_sub()
843 current_menu = upper.top();
844 upper.pop();
847 bool wxwin_mainwindow::menu_ischecked(int id)
849 CHECK_UI_THREAD;
850 if(checkitems.count(id))
851 return checkitems[id]->IsChecked();
852 else
853 return false;
856 void wxwin_mainwindow::menu_check(int id, bool newstate)
858 CHECK_UI_THREAD;
859 if(checkitems.count(id))
860 return checkitems[id]->Check(newstate);
861 else
862 return;
865 void wxwin_mainwindow::menu_enable(int id, bool newstate)
867 CHECK_UI_THREAD;
868 auto item = menubar->FindItem(id);
869 if(!item)
870 return;
871 item->Enable(newstate);
874 void wxwin_mainwindow::menu_separator()
876 CHECK_UI_THREAD;
877 current_menu->AppendSeparator();
880 void wxwin_mainwindow::panel::request_paint()
882 CHECK_UI_THREAD;
883 Refresh();
886 std::pair<double, double> calc_scale_factors(double factor, bool ar, double par)
888 if(!ar)
889 return std::make_pair(factor, factor);
890 else if(par < 1) {
891 //Too wide, make taller.
892 return std::make_pair(factor, factor / par);
893 } else {
894 //Too narrow, make wider.
895 return std::make_pair(factor * par, factor);
899 void wxwin_mainwindow::panel::on_paint(wxPaintEvent& e)
901 CHECK_UI_THREAD;
902 if(wx_escape_count >= 3 && is_fs) {
903 //Leave fullscreen mode.
904 main_window->enter_or_leave_fullscreen(false);
906 inst.fbuf->render_framebuffer();
907 static
908 uint8_t* srcp[1];
909 int srcs[1];
910 uint8_t* dstp[1];
911 int dsts[1];
912 wxPaintDC dc(this);
913 uint32_t tw, th;
914 bool aux = hflip_enabled || vflip_enabled || rotate_enabled;
915 auto sfactors = calc_scale_factors(video_scale_factor, arcorrect_enabled, inst.rom->get_PAR());
916 if(rotate_enabled) {
917 tw = inst.fbuf->main_screen.get_height() * sfactors.second + 0.5;
918 th = inst.fbuf->main_screen.get_width() * sfactors.first + 0.5;
919 } else {
920 tw = inst.fbuf->main_screen.get_width() * sfactors.first + 0.5;
921 th = inst.fbuf->main_screen.get_height() * sfactors.second + 0.5;
923 if(!tw || !th) {
924 main_window_dirty = false;
925 return;
927 //Scale this to fullscreen.
928 unsigned dx = 0, dy = 0;
929 if(is_fs) {
930 wxSize screen = main_window->GetSize();
931 double fss = min(1.0 * screen.GetWidth() / tw, 1.0 * screen.GetHeight() / th);
932 tw *= fss;
933 th *= fss;
934 if((signed)tw < screen.GetWidth())
935 dx = (screen.GetWidth() - tw) / 2;
936 if((signed)th < screen.GetHeight())
937 dy = (screen.GetHeight() - th) / 2;
938 if(becoming_fullscreen) {
939 //Force panel to fullscreen.
940 SetSize(screen);
941 Move(0, 0);
942 becoming_fullscreen = false;
944 //Erase borders.
945 signed dx2 = dx + tw;
946 signed dy2 = dy + th;
947 dc.SetBrush(*wxBLACK_BRUSH);
948 dc.SetPen(*wxBLACK_PEN);
949 //Erase the borders we don't draw.
950 if(dx > 0) dc.DrawRectangle(0, 0, dx, screen.GetHeight());
951 if(dy > 0) dc.DrawRectangle(0, 0, screen.GetWidth(), dy);
952 if(dx2 < screen.GetWidth()) dc.DrawRectangle(dx2, 0, screen.GetWidth() - dx2, screen.GetHeight());
953 if(dy2 < screen.GetHeight()) dc.DrawRectangle(0, dy2, screen.GetWidth(), screen.GetHeight() - dy2);
956 if(!screen_buffer || tw != old_width || th != old_height || scaling_flags != old_flags ||
957 hflip_enabled != old_hflip || vflip_enabled != old_vflip || rotate_enabled != old_rotate) {
958 if(screen_buffer) {
959 delete[] screen_buffer;
960 screen_buffer = NULL;
962 if(rotate_buffer) {
963 delete[] rotate_buffer;
964 rotate_buffer = NULL;
966 old_height = th;
967 old_width = tw;
968 old_flags = scaling_flags;
969 old_hflip = hflip_enabled;
970 old_vflip = vflip_enabled;
971 old_rotate = rotate_enabled;
972 uint32_t w = inst.fbuf->main_screen.get_width();
973 uint32_t h = inst.fbuf->main_screen.get_height();
974 if(w && h)
975 sws_ctx = sws_getCachedContext(sws_ctx, rotate_enabled ? h : w, rotate_enabled ? w : h,
976 PIX_FMT_RGBA, tw, th, PIX_FMT_BGR24, scaling_flags, NULL, NULL, NULL);
977 tw = max(tw, static_cast<uint32_t>(128));
978 th = max(th, static_cast<uint32_t>(112));
979 screen_buffer = new unsigned char[tw * th * 3 + 64];
980 if(aux)
981 rotate_buffer = new uint32_t[inst.fbuf->main_screen.get_width() *
982 inst.fbuf->main_screen.get_height()];
983 if(!is_fs) {
984 //This is not preformed in fullscreen mode.
985 SetMinSize(wxSize(tw, th));
986 signal_resize_needed();
989 if(aux) {
990 //Hflip, Vflip or rotate active.
991 size_t width = inst.fbuf->main_screen.get_width();
992 size_t height = inst.fbuf->main_screen.get_height();
993 size_t width1 = width - 1;
994 size_t height1 = height - 1;
995 size_t stride = inst.fbuf->main_screen.rowptr(1) - inst.fbuf->main_screen.rowptr(0);
996 uint32_t* pixels = inst.fbuf->main_screen.rowptr(0);
997 if(rotate_enabled) {
998 for(unsigned y = 0; y < height; y++) {
999 uint32_t* pixels2 = pixels + (vflip_enabled ? (height1 - y) : y) * stride;
1000 uint32_t* dpixels = rotate_buffer + (height1 - y);
1001 if(hflip_enabled)
1002 for(unsigned x = 0; x < width; x++)
1003 dpixels[x * height] = pixels2[width1 - x];
1004 else
1005 for(unsigned x = 0; x < width; x++)
1006 dpixels[x * height] = pixels2[x];
1008 } else {
1009 for(unsigned y = 0; y < height; y++) {
1010 uint32_t* pixels2 = pixels + (vflip_enabled ? (height1 - y) : y) * stride;
1011 uint32_t* dpixels = rotate_buffer + y * width;
1012 if(hflip_enabled)
1013 for(unsigned x = 0; x < width; x++)
1014 dpixels[x] = pixels2[width1 - x];
1015 else
1016 for(unsigned x = 0; x < width; x++)
1017 dpixels[x] = pixels2[x];
1021 if(aux)
1022 srcs[0] = 4 * (rotate_enabled ? inst.fbuf->main_screen.get_height() :
1023 inst.fbuf->main_screen.get_width());
1024 else
1025 srcs[0] = 4 * inst.fbuf->main_screen.get_stride();
1026 dsts[0] = 3 * tw;
1027 srcp[0] = reinterpret_cast<unsigned char*>(aux ? rotate_buffer : inst.fbuf->main_screen.rowptr(0));
1028 dstp[0] = screen_buffer;
1029 memset(screen_buffer, 0, tw * th * 3);
1030 if(inst.fbuf->main_screen.get_width() && inst.fbuf->main_screen.get_height())
1031 sws_scale(sws_ctx, srcp, srcs, 0, rotate_enabled ? inst.fbuf->main_screen.get_width() :
1032 inst.fbuf->main_screen.get_height(),
1033 dstp, dsts);
1034 wxBitmap bmp(wxImage(tw, th, screen_buffer, true));
1035 dc.DrawBitmap(bmp, dx, dy, false);
1036 main_window_dirty = false;
1037 main_window->update_statusbar();
1040 void wxwin_mainwindow::panel::on_erase(wxEraseEvent& e)
1042 //Blank.
1045 void wxwin_mainwindow::panel::on_keyboard_down(wxKeyEvent& e)
1047 CHECK_UI_THREAD;
1048 handle_wx_keyboard(inst, e, true);
1051 void wxwin_mainwindow::panel::on_keyboard_up(wxKeyEvent& e)
1053 CHECK_UI_THREAD;
1054 handle_wx_keyboard(inst, e, false);
1055 if(wx_escape_count >= 3 && is_fs) {
1056 //Force leave fullscreen mode.
1057 main_window->enter_or_leave_fullscreen(false);
1061 void wxwin_mainwindow::panel::on_mouse(wxMouseEvent& e)
1063 CHECK_UI_THREAD;
1064 handle_wx_mouse(inst, e);
1067 wxwin_mainwindow::wxwin_mainwindow(emulator_instance& _inst, bool fscreen)
1068 : wxFrame(NULL, wxID_ANY, getname(_inst), wxDefaultPosition, wxSize(-1, -1),
1069 wxMINIMIZE_BOX | wxSYSTEM_MENU | wxCAPTION | wxCLIP_CHILDREN | wxCLOSE_BOX), inst(_inst)
1071 CHECK_UI_THREAD;
1072 download_in_progress = NULL;
1073 Centre();
1074 mwindow = NULL;
1075 toplevel = new wxFlexGridSizer(1, 2, 0, 0);
1076 toplevel->Add(gpanel = new panel(this, inst), 1, wxGROW);
1077 toplevel->Add(spanel = new wxwin_status::panel(this, inst, gpanel, 20), 1, wxGROW);
1078 spanel_shown = true;
1079 toplevel->SetSizeHints(this);
1080 SetSizer(toplevel);
1081 Fit();
1082 gpanel->SetFocus();
1083 Connect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(wxwin_mainwindow::on_close));
1084 SetMenuBar(menubar = new wxMenuBar);
1085 SetStatusBar(statusbar = new wxStatusBar(this));
1087 menu_start(wxT("File"));
1088 menu_start_sub(wxT("New"));
1089 menu_entry(wxID_NEW_MOVIE, wxT("Movie..."));
1090 menu_entry(wxID_NEW_PROJECT, wxT("Project..."));
1091 menu_end_sub();
1092 menu_start_sub(wxT("Load"));
1093 menu_entry(wxID_LOAD_STATE, wxT("State..."));
1094 menu_entry(wxID_LOAD_MOVIE, wxT("Movie..."));
1095 menu_entry(wxID_DOWNLOAD, wxT("Download movie..."));
1096 if(loadlib::library::name() != "") {
1097 menu_separator();
1098 menu_entry(wxID_LOAD_LIBRARY, towxstring(std::string("Load ") + loadlib::library::name()));
1099 menu_entry(wxID_PLUGIN_MANAGER, towxstring("Plugin manager"));
1101 menu_separator();
1102 menu_entry(wxID_RELOAD_ROM_IMAGE, wxT("Reload ROM"));
1103 menu_entry(wxID_LOAD_ROM_IMAGE_FIRST, wxT("ROM..."));
1104 menu_special_sub(wxT("Multifile ROM"), loadroms = new loadrom_menu(this, wxID_LOAD_ROM_IMAGE_FIRST + 1,
1105 wxID_LOAD_ROM_IMAGE_LAST, [this](core_type* t) { this->do_load_rom_image(t); }));
1106 menu_special_sub(wxT("Project"), projects = new projects_menu(this, inst, wxID_PROJECT_FIRST,
1107 wxID_PROJECT_LAST, get_config_path() + "/recent-projects.txt", [this](const std::string& id) {
1108 this->project_selected(id); }));
1109 menu_separator();
1110 menu_special_sub(wxT("Recent ROMs"), recent_roms = new recent_menu<recentfiles::multirom>(this, inst,
1111 wxID_RROM_FIRST, wxID_RROM_LAST, get_config_path() + "/recent-roms.txt", recent_rom_selected));
1112 menu_special_sub(wxT("Recent Movies"), recent_movies = new recent_menu<recentfiles::path>(this, inst,
1113 wxID_RMOVIE_FIRST, wxID_RMOVIE_LAST, get_config_path() + "/recent-movies.txt",
1114 recent_movie_selected));
1115 menu_special_sub(wxT("Recent Lua scripts"), recent_scripts = new recent_menu<recentfiles::path>(this, inst,
1116 wxID_RLUA_FIRST, wxID_RLUA_LAST, get_config_path() + "/recent-scripts.txt",
1117 recent_script_selected));
1118 menu_separator();
1119 menu_entry(wxID_CONFLICTRESOLUTION, wxT("Conflict resolution"));
1120 menu_separator();
1121 branches_menu* brlist;
1122 auto brlist_item = menu_special_sub(wxT("Branches"), brlist = new branches_menu(this, inst,
1123 wxID_BRANCH_FIRST, wxID_BRANCH_LAST));
1124 brlist->set_disabler([brlist_item](bool enabled) { brlist_item->Enable(enabled); });
1125 brlist->update();
1126 menu_end_sub();
1127 menu_start_sub(wxT("Save"));
1128 menu_entry(wxID_SAVE_STATE, wxT("State..."));
1129 menu_entry(wxID_SAVE_MOVIE, wxT("Movie..."));
1130 menu_entry(wxID_SAVE_SCREENSHOT, wxT("Screenshot..."));
1131 menu_entry(wxID_SAVE_SUBTITLES, wxT("Subtitles..."));
1132 menu_entry(wxID_CANCEL_SAVES, wxT("Cancel pending saves"));
1133 menu_separator();
1134 menu_entry(wxID_CHDIR, wxT("Change working directory..."));
1135 menu_separator();
1136 menu_special_sub(wxT("Upload"), new upload_menu(this, inst, wxID_UPLOAD_FIRST, wxID_UPLOAD_LAST));
1137 menu_end_sub();
1138 menu_start_sub(wxT("Close"));
1139 menu_entry(wxID_CLOSE_PROJECT, wxT("Project"));
1140 menu_entry(wxID_CLOSE_ROM, wxT("ROM"));
1141 menu_enable(wxID_CLOSE_PROJECT, inst.project->get() != NULL);
1142 menu_enable(wxID_CLOSE_ROM, inst.project->get() == NULL);
1143 menu_end_sub();
1144 menu_separator();
1145 menu_entry(wxID_EXIT, wxT("Quit"));
1147 menu_special(wxT("System"), reinterpret_cast<wxMenu*>(sysmenu = new system_menu(this, inst)));
1149 menu_start(wxT("Movie"));
1150 menu_entry_check(wxID_READONLY_MODE, wxT("Readonly mode"));
1151 menu_check(wxID_READONLY_MODE, is_readonly_mode(inst));
1152 menu_entry(wxID_EDIT_AUTHORS, wxT("Edit game name && authors..."));
1153 menu_entry(wxID_EDIT_SUBTITLES, wxT("Edit subtitles..."));
1154 menu_entry(wxID_EDIT_VSUBTITLES, wxT("Edit commantary track..."));
1155 menu_separator();
1156 menu_entry(wxID_REWIND_MOVIE, wxT("Rewind to start"));
1158 menu_start(wxT("Speed"));
1159 menu_entry(wxID_SPEED_5, wxT("1/20x"));
1160 menu_entry(wxID_SPEED_10, wxT("1/10x"));
1161 menu_entry(wxID_SPEED_17, wxT("1/6x"));
1162 menu_entry(wxID_SPEED_20, wxT("1/5x"));
1163 menu_entry(wxID_SPEED_25, wxT("1/4x"));
1164 menu_entry(wxID_SPEED_33, wxT("1/3x"));
1165 menu_entry(wxID_SPEED_50, wxT("1/2x"));
1166 menu_entry(wxID_SPEED_100, wxT("1x"));
1167 menu_entry(wxID_SPEED_150, wxT("1.5x"));
1168 menu_entry(wxID_SPEED_200, wxT("2x"));
1169 menu_entry(wxID_SPEED_300, wxT("3x"));
1170 menu_entry(wxID_SPEED_500, wxT("5x"));
1171 menu_entry(wxID_SPEED_1000, wxT("10x"));
1172 menu_entry(wxID_SPEED_TURBO, wxT("Turbo"));
1173 menu_entry(wxID_SET_SPEED, wxT("Set..."));
1175 menu_start(wxT("Tools"));
1176 menu_entry(wxID_RUN_SCRIPT, wxT("Run batch file..."));
1177 menu_separator();
1178 menu_entry(wxID_EVAL_LUA, wxT("Evaluate Lua statement..."));
1179 menu_entry(wxID_RUN_LUA, wxT("Run Lua script..."));
1180 menu_separator();
1181 menu_entry(wxID_RESET_LUA, wxT("Reset Lua VM"));
1182 menu_separator();
1183 menu_entry(wxID_AUTOHOLD, wxT("Autohold/Autofire..."));
1184 menu_entry(wxID_TASINPUT, wxT("TAS input plugin..."));
1185 menu_entry(wxID_MULTITRACK, wxT("Multitrack..."));
1186 menu_entry(wxID_EDIT_MACROS, wxT("Edit macros..."));
1187 menu_separator();
1188 menu_entry(wxID_EDIT_MEMORYWATCH, wxT("Edit memory watch..."));
1189 menu_separator();
1190 menu_entry(wxID_LOAD_MEMORYWATCH, wxT("Load memory watch..."));
1191 menu_entry(wxID_SAVE_MEMORYWATCH, wxT("Save memory watch..."));
1192 menu_separator();
1193 menu_entry(wxID_MEMORY_SEARCH, wxT("Memory Search..."));
1194 menu_entry(wxID_HEXEDITOR, wxT("Memory editor..."));
1195 tracelog_menu* trlog;
1196 auto trlog_item = menu_special_sub(wxT("Trace log"), trlog = new tracelog_menu(this, inst,
1197 wxID_TRACELOG_FIRST, wxID_TRACELOG_LAST));
1198 trlog->set_disabler([trlog_item](bool enabled) { trlog_item->Enable(enabled); });
1199 trlog->update();
1200 menu_entry(wxID_DISASSEMBLER, wxT("Disassembler..."));
1201 menu_separator();
1202 menu_entry(wxID_MOVIE_EDIT, wxT("Edit movie..."));
1203 menu_separator();
1204 menu_special_sub(wxT("Video Capture"), reinterpret_cast<dumper_menu*>(dmenu = new dumper_menu(this,
1205 inst, wxID_DUMP_FIRST, wxID_DUMP_LAST)));
1207 menu_start(wxT("Configure"));
1208 menu_entry_check(wxID_SHOW_STATUS, wxT("Show status panel"));
1209 menu_check(wxID_SHOW_STATUS, true);
1210 menu_entry_check(wxID_DEDICATED_MEMORY_WATCH, wxT("Dedicated memory watch"));
1211 menu_entry(wxID_SHOW_MESSAGES, wxT("Show messages"));
1212 menu_special_sub(wxT("Settings"), new settings_menu(this, inst, wxID_SETTINGS_FIRST));
1213 if(audioapi_driver_initialized()) {
1214 menu_separator();
1215 menu_entry_check(wxID_AUDIO_ENABLED, wxT("Sounds enabled"));
1216 menu_entry(wxID_VUDISPLAY, wxT("VU display / sound controls"));
1217 menu_check(wxID_AUDIO_ENABLED, platform::is_sound_enabled());
1219 menu_separator();
1220 menu_entry(wxID_ENTER_FULLSCREEN, wxT("Enter fullscreen mode"));
1222 menu_start(wxT("Help"));
1223 menu_entry(wxID_ABOUT, wxT("About..."));
1225 corechange.set(inst.dispatch->core_change, []() { signal_core_change(); });
1226 titlechange.set(inst.dispatch->title_change, []() { signal_core_change(); });
1227 newcore.set(notify_new_core, []() { update_preferences(); });
1228 unmuted.set(inst.dispatch->sound_unmute, [this](bool unmute) {
1229 runuifun([this, unmute]() { this->menu_check(wxID_AUDIO_ENABLED, unmute); });
1231 modechange.set(inst.dispatch->mode_change, [this](bool readonly) {
1232 runuifun([this, readonly]() { this->menu_check(wxID_READONLY_MODE, readonly); });
1234 gpanel->SetDropTarget(new loadfile(this, inst));
1235 spanel->SetDropTarget(new loadfile(this, inst));
1236 set_hasher_callback(hash_callback);
1237 reinterpret_cast<system_menu*>(sysmenu)->update(false);
1238 menubar->SetMenuLabel(1, towxstring(inst.rom->get_systemmenu_name()));
1239 focus_timer = new _focus_timer;
1240 status_timer = new _status_timer;
1241 if(fscreen) {
1242 wx_escape_count = 0;
1243 enter_or_leave_fullscreen(true);
1247 wxwin_mainwindow::~wxwin_mainwindow()
1249 CHECK_UI_THREAD;
1250 if(sws_ctx) sws_freeContext(sws_ctx);
1251 if(screen_buffer) delete[] screen_buffer;
1252 if(rotate_buffer) delete[] rotate_buffer;
1253 focus_timer->Stop();
1254 delete focus_timer;
1255 status_timer->Stop();
1256 delete status_timer;
1259 void wxwin_mainwindow::request_paint()
1261 CHECK_UI_THREAD;
1262 gpanel->Refresh();
1265 void wxwin_mainwindow::on_close(wxCloseEvent& e)
1267 CHECK_UI_THREAD;
1268 //Veto it for now, latter things will delete it.
1269 e.Veto();
1270 inst.iqueue->queue("quit-emulator");
1273 void wxwin_mainwindow::notify_update() throw()
1275 CHECK_UI_THREAD;
1276 if(!main_window_dirty) {
1277 main_window_dirty = true;
1278 gpanel->Refresh();
1282 void wxwin_mainwindow::notify_resized() throw()
1284 CHECK_UI_THREAD;
1285 toplevel->Layout();
1286 toplevel->SetSizeHints(this);
1287 Fit();
1290 void wxwin_mainwindow::notify_update_status() throw()
1292 CHECK_UI_THREAD;
1293 spanel->request_paint();
1294 if(mwindow)
1295 mwindow->notify_update();
1296 status_updated = true;
1299 void wxwin_mainwindow::notify_exit() throw()
1301 CHECK_UI_THREAD;
1302 wxwidgets_exiting = true;
1303 join_emulator_thread();
1304 Destroy();
1307 std::string read_variable_map(const std::map<std::string, std::u32string>& vars, const std::string& key)
1309 if(!vars.count(key))
1310 return "";
1311 return utf8::to8(vars.find(key)->second);
1314 void wxwin_mainwindow::update_statusbar()
1316 CHECK_UI_THREAD;
1317 if(download_in_progress) {
1318 statusbar->SetStatusText(towxstring(download_in_progress->statusmsg()));
1319 return;
1321 if(hashing_in_progress) {
1322 //TODO: Display this as a dialog.
1323 std::ostringstream s;
1324 s << "Hashing ROMs, approximately " << ((hashing_left + 524288) >> 20) << " of "
1325 << ((hashing_total + 524288) >> 20) << "MB left...";
1326 statusbar->SetStatusText(towxstring(s.str()));
1327 return;
1329 auto& vars = inst.status->get_read();
1330 if(!vars.valid) {
1331 inst.status->put_read();
1332 return;
1334 try {
1335 std::ostringstream s;
1336 bool recording = (vars.mode == 'R');
1337 if(vars.movie_valid) {
1338 if(recording)
1339 s << "Frame: " << vars.curframe;
1340 else
1341 s << "Frame: " << vars.curframe << "/" << vars.length;
1342 s << " Lag: " << vars.lag;
1343 if(vars.subframe == _lsnes_status::subframe_savepoint)
1344 s << " Subframe: S";
1345 else if(vars.subframe == _lsnes_status::subframe_video)
1346 s << " Subframe: V";
1347 else
1348 s << " Subframe: " << vars.subframe;
1349 } else {
1350 s << "Frame: N/A Lag: N/A Subframe: N/A";
1352 if(vars.saveslot_valid) {
1353 s << " Slot: ";
1354 if(vars.branch_valid) s << utf8::to8(vars.branch) << "ā†’";
1355 s << vars.saveslot;
1356 s << " [" << utf8::to8(vars.slotinfo) << "]";
1358 s << " Speed: " << vars.speed << "% ";
1359 if(vars.pause == _lsnes_status::pause_break)
1360 s << " Breakpoint";
1361 else if(vars.pause == _lsnes_status::pause_normal)
1362 s << " Paused";
1363 if(vars.dumping)
1364 s << " Dumping";
1365 if(vars.mode == 'C')
1366 s << " Corrupt";
1367 else if(vars.mode == 'R')
1368 s << " Recording";
1369 else if(vars.mode == 'P')
1370 s << " Playback";
1371 else if(vars.mode == 'F')
1372 s << " Finished";
1373 else
1374 s << " Unknown";
1375 if(vars.mbranch_valid)
1376 s << " Branch: " << utf8::to8(vars.mbranch);
1377 std::string macros = utf8::to8(vars.macros);
1378 if(macros.length())
1379 s << " Macros: " << macros;
1381 statusbar->SetStatusText(towxstring(s.str()));
1382 } catch(std::exception& e) {
1384 inst.status->put_read();
1387 #define NEW_KEYBINDING "A new binding..."
1388 #define NEW_ALIAS "A new alias..."
1389 #define NEW_WATCH "A new watch..."
1391 void wxwin_mainwindow::handle_menu_click(wxCommandEvent& e)
1393 CHECK_UI_THREAD;
1394 try {
1395 handle_menu_click_cancelable(e);
1396 } catch(canceled_exception& e) {
1397 //Ignore.
1398 } catch(std::bad_alloc& e) {
1399 OOM_panic();
1400 } catch(std::exception& e) {
1401 show_message_ok(this, "Error in menu handler", e.what(), wxICON_EXCLAMATION);
1405 void wxwin_mainwindow::refresh_title() throw()
1407 CHECK_UI_THREAD;
1408 SetTitle(getname(inst));
1409 auto p = inst.project->get();
1410 menu_enable(wxID_RELOAD_ROM_IMAGE, !p);
1411 for(int i = wxID_LOAD_ROM_IMAGE_FIRST; i <= wxID_LOAD_ROM_IMAGE_LAST; i++)
1412 menu_enable(i, !p);
1413 menu_enable(wxID_CLOSE_PROJECT, p != NULL);
1414 menu_enable(wxID_CLOSE_ROM, p == NULL);
1415 reinterpret_cast<system_menu*>(sysmenu)->update(false);
1416 menubar->SetMenuLabel(1, towxstring(inst.rom->get_systemmenu_name()));
1419 namespace
1421 struct movie_or_savestate
1423 public:
1424 typedef std::pair<std::string,std::string> returntype;
1425 movie_or_savestate(emulator_instance& _inst, bool is_state)
1426 : inst(_inst)
1428 state = is_state;
1430 filedialog_input_params input(bool save) const
1432 filedialog_input_params p;
1433 std::string ext = state ? inst.project->savestate_ext() : "lsmv";
1434 std::string name = state ? "Savestates" : "Movies";
1435 if(save) {
1436 p.types.push_back(filedialog_type_entry(name, "*." + ext, ext));
1437 p.types.push_back(filedialog_type_entry(name + " (binary)", "*." + ext, ext));
1438 } else
1439 p.types.push_back(filedialog_type_entry(name, "*." + ext + ";*." + ext + ".backup",
1440 ext));
1441 if(!save && state) {
1442 p.types.push_back(filedialog_type_entry("Savestates [playback]", "*." + ext +
1443 ";*." + ext + ".backup", ext));
1444 p.types.push_back(filedialog_type_entry("Savestates [recording]", "*." + ext +
1445 ";*." + ext + ".backup", ext));
1446 p.types.push_back(filedialog_type_entry("Savestates [preserve]", "*." + ext +
1447 ";*." + ext + ".backup", ext));
1448 p.types.push_back(filedialog_type_entry("Savestates [all branches]", "*." + ext +
1449 ";*." + ext + ".backup", ext));
1451 p.default_type = save ? (state ? save_dflt_binary(*inst.settings) :
1452 movie_dflt_binary(*inst.settings)) : 0;
1453 return p;
1455 std::pair<std::string, std::string> output(const filedialog_output_params& p, bool save) const
1457 std::string cmdmod;
1458 if(save)
1459 cmdmod = p.typechoice ? "-binary" : "-zip";
1460 else if(state)
1461 switch(p.typechoice) {
1462 case 0: cmdmod = ""; break;
1463 case 1: cmdmod = "-readonly"; break;
1464 case 2: cmdmod = "-state"; break;
1465 case 3: cmdmod = "-preserve"; break;
1466 case 4: cmdmod = "-allbranches"; break;
1468 return std::make_pair(cmdmod, p.path);
1470 private:
1471 emulator_instance& inst;
1472 bool state;
1474 struct movie_or_savestate filetype_movie(lsnes_instance, false);
1475 struct movie_or_savestate filetype_savestate(lsnes_instance, true);
1478 void wxwin_mainwindow::project_selected(const std::string& id)
1480 std::string filename, displayname;
1481 bool load_ok = false;
1482 inst.iqueue->run([id, &filename, &displayname, &load_ok]() -> void {
1483 try {
1484 auto& p = CORE().project->load(id); //Check.
1485 filename = p.filename;
1486 displayname = p.name;
1487 load_ok = true;
1488 delete &p;
1489 switch_projects(id);
1490 } catch(std::exception& e) {
1491 messages << "Failed to change project: " << e.what() << std::endl;
1494 if(load_ok) {
1495 recentfiles::namedobj obj;
1496 obj._id = id;
1497 obj._filename = filename;
1498 obj._display = displayname;
1499 projects->add(obj);
1503 void wxwin_mainwindow::handle_menu_click_cancelable(wxCommandEvent& e)
1505 CHECK_UI_THREAD;
1506 std::string filename;
1507 std::pair<std::string, std::string> filename2;
1508 bool s;
1509 switch(e.GetId()) {
1510 case wxID_FRAMEADVANCE:
1511 inst.iqueue->queue("+advance-frame");
1512 inst.iqueue->queue("-advance-frame");
1513 return;
1514 case wxID_SUBFRAMEADVANCE:
1515 inst.iqueue->queue("+advance-poll");
1516 inst.iqueue->queue("-advance-poll");
1517 return;
1518 case wxID_NEXTPOLL:
1519 inst.iqueue->queue("advance-skiplag");
1520 return;
1521 case wxID_PAUSE:
1522 inst.iqueue->queue("pause-emulator");
1523 return;
1524 case wxID_EXIT:
1525 inst.iqueue->queue("quit-emulator");
1526 return;
1527 case wxID_AUDIO_ENABLED:
1528 platform::sound_enable(menu_ischecked(wxID_AUDIO_ENABLED));
1529 return;
1530 case wxID_CANCEL_SAVES:
1531 inst.iqueue->queue("cancel-saves");
1532 return;
1533 case wxID_LOAD_MOVIE:
1534 filename = choose_file_load(this, "Load Movie", UI_get_project_moviepath(inst),
1535 filetype_movie).second;
1536 recent_movies->add(filename);
1537 inst.iqueue->queue(CLOADSAVE::ldm.name, filename);
1538 return;
1539 case wxID_LOAD_STATE:
1540 filename2 = choose_file_load(this, "Load State", UI_get_project_moviepath(inst),
1541 filetype_savestate);
1542 recent_movies->add(filename2.second);
1543 inst.iqueue->queue("load" + filename2.first + " " + filename2.second);
1544 return;
1545 case wxID_REWIND_MOVIE:
1546 inst.iqueue->queue("rewind-movie");
1547 return;
1548 case wxID_SAVE_MOVIE:
1549 filename2 = choose_file_save(this, "Save Movie", UI_get_project_moviepath(inst), filetype_movie,
1550 project_prefixname(inst, "lsmv"));
1551 recent_movies->add(filename2.second);
1552 inst.iqueue->queue("save-movie" + filename2.first + " " + filename2.second);
1553 return;
1554 case wxID_SAVE_SUBTITLES:
1555 inst.iqueue->queue(CSUBTITLE::save.name, choose_file_save(this, "Save subtitles",
1556 UI_get_project_moviepath(inst), filetype_sub, project_prefixname(inst, "sub")));
1557 return;
1558 case wxID_SAVE_STATE:
1559 filename2 = choose_file_save(this, "Save State", UI_get_project_moviepath(inst),
1560 filetype_savestate);
1561 recent_movies->add(filename2.second);
1562 inst.iqueue->queue("save-state" + filename2.first + " " + filename2.second);
1563 return;
1564 case wxID_SAVE_SCREENSHOT:
1565 inst.iqueue->queue(CFRAMEBUF::ss.name, choose_file_save(this, "Save Screenshot",
1566 UI_get_project_moviepath(inst), filetype_png, get_default_screenshot_name(inst)));
1567 return;
1568 case wxID_RUN_SCRIPT:
1569 inst.iqueue->queue(CLUA::run.name, pick_file_member(this, "Select Script",
1570 UI_get_project_otherpath(inst)));
1571 return;
1572 case wxID_RUN_LUA: {
1573 std::string f = choose_file_load(this, "Select Lua Script", UI_get_project_otherpath(inst),
1574 filetype_lua_script);
1575 inst.iqueue->queue(CLUA::run.name, f);
1576 recent_scripts->add(f);
1577 return;
1579 case wxID_RESET_LUA:
1580 inst.iqueue->queue(CLUA::reset.name);
1581 return;
1582 case wxID_EVAL_LUA:
1583 inst.iqueue->queue(CLUA::eval.name, pick_text(this, "Evaluate Lua", "Enter Lua Statement:"));
1584 return;
1585 case wxID_READONLY_MODE:
1586 s = menu_ischecked(wxID_READONLY_MODE);
1587 inst.iqueue->run([s]() {
1588 auto& core = CORE();
1589 if(!s)
1590 core.lua2->callback_movie_lost("readwrite");
1591 if(*core.mlogic) core.mlogic->get_movie().readonly_mode(s);
1592 core.dispatch->mode_change(s);
1593 if(!s)
1594 core.lua2->callback_do_readwrite();
1595 core.supdater->update();
1596 core.dispatch->status_update();
1598 return;
1599 case wxID_AUTOHOLD:
1600 wxeditor_autohold_display(this, inst);
1601 return;
1602 case wxID_EDIT_AUTHORS:
1603 wxeditor_authors_display(this, inst);
1604 return;
1605 case wxID_EDIT_MACROS:
1606 wxeditor_macro_display(this, inst);
1607 return;
1608 case wxID_EDIT_SUBTITLES:
1609 wxeditor_subtitles_display(this, inst);
1610 return;
1611 case wxID_EDIT_VSUBTITLES:
1612 show_wxeditor_voicesub(this, inst);
1613 return;
1614 case wxID_EDIT_MEMORYWATCH:
1615 wxeditor_memorywatches_display(this, inst);
1616 return;
1617 case wxID_SAVE_MEMORYWATCH: {
1618 modal_pause_holder hld;
1619 std::set<std::string> old_watches;
1620 inst.iqueue->run([&old_watches]() { old_watches = CORE().mwatch->enumerate(); });
1621 std::string filename = choose_file_save(this, "Save watches to file",
1622 UI_get_project_otherpath(inst), filetype_watch);
1623 std::ofstream out(filename.c_str());
1624 for(auto i : old_watches) {
1625 std::string val;
1626 inst.iqueue->run([i, &val]() {
1627 try {
1628 val = CORE().mwatch->get_string(i);
1629 } catch(std::exception& e) {
1630 messages << "Can't get value of watch '" << i << "': " << e.what()
1631 << std::endl;
1634 out << i << std::endl << val << std::endl;
1636 out.close();
1637 return;
1639 case wxID_LOAD_MEMORYWATCH: {
1640 modal_pause_holder hld;
1641 std::set<std::string> old_watches;
1642 inst.iqueue->run([&old_watches]() { old_watches = CORE().mwatch->enumerate(); });
1643 std::map<std::string, std::string> new_watches;
1644 std::string filename = choose_file_load(this, "Choose memory watch file",
1645 UI_get_project_otherpath(inst), filetype_watch);
1646 try {
1647 std::istream& in = zip::openrel(filename, "");
1648 while(in) {
1649 std::string wname;
1650 std::string wexpr;
1651 std::getline(in, wname);
1652 std::getline(in, wexpr);
1653 new_watches[strip_CR(wname)] = strip_CR(wexpr);
1655 delete &in;
1656 } catch(std::exception& e) {
1657 show_message_ok(this, "Error", std::string("Can't load memory watch: ") + e.what(),
1658 wxICON_EXCLAMATION);
1659 return;
1662 inst.iqueue->run([this, &new_watches, &old_watches]() {
1663 handle_watch_load(this->inst, new_watches, old_watches);
1665 return;
1667 case wxID_MEMORY_SEARCH:
1668 wxwindow_memorysearch_display(inst);
1669 return;
1670 case wxID_TASINPUT:
1671 wxeditor_tasinput_display(this, inst);
1672 return;
1673 case wxID_ABOUT: {
1674 std::ostringstream str;
1675 str << "Version: lsnes rr" << lsnes_version << std::endl;
1676 str << "Revision: " << lsnes_git_revision << std::endl;
1677 for(auto i : core_core::all_cores())
1678 if(!i->is_hidden())
1679 str << "Core: " << i->get_core_identifier() << std::endl;
1680 wxMessageBox(towxstring(str.str()), _T("About"), wxICON_INFORMATION | wxOK, this);
1681 return;
1683 case wxID_SHOW_STATUS: {
1684 bool newstate = menu_ischecked(wxID_SHOW_STATUS);
1685 if(newstate)
1686 spanel->Show();
1687 if(newstate && !spanel_shown)
1688 toplevel->Add(spanel, 1, wxGROW);
1689 else if(!newstate && spanel_shown)
1690 toplevel->Detach(spanel);
1691 if(!newstate)
1692 spanel->Hide();
1693 spanel_shown = newstate;
1694 toplevel->Layout();
1695 toplevel->SetSizeHints(this);
1696 Fit();
1697 return;
1699 case wxID_DEDICATED_MEMORY_WATCH: {
1700 bool newstate = menu_ischecked(wxID_DEDICATED_MEMORY_WATCH);
1701 if(newstate && !mwindow) {
1702 mwindow = new wxwin_status(-1, inst, "Memory Watch");
1703 spanel->set_watch_flag(1);
1704 mwindow->Show();
1705 } else if(!newstate && mwindow) {
1706 mwindow->Destroy();
1707 mwindow = NULL;
1708 spanel->set_watch_flag(0);
1710 return;
1712 case wxID_SET_SPEED: {
1713 std::string value = "infinite";
1714 double val = inst.framerate->get_speed_multiplier();
1715 if(!(val == std::numeric_limits<double>::infinity()))
1716 value = (stringfmt() << (100 * val)).str();
1717 value = pick_text(this, "Set speed", "Enter percentage speed (or \"infinite\"):", value);
1718 try {
1719 if(value == "infinite")
1720 inst.framerate->set_speed_multiplier(
1721 std::numeric_limits<double>::infinity());
1722 else {
1723 double v = parse_value<double>(value) / 100;
1724 if(v <= 0.0001)
1725 throw 42;
1726 inst.framerate->set_speed_multiplier(v);
1728 } catch(...) {
1729 wxMessageBox(wxT("Invalid speed"), _T("Error"), wxICON_EXCLAMATION | wxOK, this);
1731 return;
1733 case wxID_SPEED_5:
1734 set_speed(inst, 5);
1735 break;
1736 case wxID_SPEED_10:
1737 set_speed(inst, 10);
1738 break;
1739 case wxID_SPEED_17:
1740 set_speed(inst, 16.66666666666);
1741 break;
1742 case wxID_SPEED_20:
1743 set_speed(inst, 20);
1744 break;
1745 case wxID_SPEED_25:
1746 set_speed(inst, 25);
1747 break;
1748 case wxID_SPEED_33:
1749 set_speed(inst, 33.3333333333333);
1750 break;
1751 case wxID_SPEED_50:
1752 set_speed(inst, 50);
1753 break;
1754 case wxID_SPEED_100:
1755 set_speed(inst, 100);
1756 break;
1757 case wxID_SPEED_150:
1758 set_speed(inst, 150);
1759 break;
1760 case wxID_SPEED_200:
1761 set_speed(inst, 200);
1762 break;
1763 case wxID_SPEED_300:
1764 set_speed(inst, 300);
1765 break;
1766 case wxID_SPEED_500:
1767 set_speed(inst, 500);
1768 break;
1769 case wxID_SPEED_1000:
1770 set_speed(inst, 1000);
1771 break;
1772 case wxID_SPEED_TURBO:
1773 set_speed(inst, -1);
1774 break;
1775 case wxID_LOAD_LIBRARY: {
1776 std::string name = std::string("load ") + loadlib::library::name();
1777 with_loaded_library(*new loadlib::module(loadlib::library(choose_file_load(this, name,
1778 UI_get_project_otherpath(inst), single_type(loadlib::library::extension(),
1779 loadlib::library::name())))));
1780 handle_post_loadlibrary();
1781 break;
1783 case wxID_PLUGIN_MANAGER:
1784 wxeditor_plugin_manager_display(this);
1785 return;
1786 case wxID_RELOAD_ROM_IMAGE:
1787 inst.iqueue->run([]() {
1788 CORE().command->invoke("unpause-emulator");
1789 reload_current_rom();
1791 return;
1792 case wxID_NEW_MOVIE:
1793 show_projectwindow(this, inst);
1794 return;
1795 case wxID_SHOW_MESSAGES:
1796 msg_window->reshow();
1797 return;
1798 case wxID_CONFLICTRESOLUTION:
1799 show_conflictwindow(this);
1800 return;
1801 case wxID_VUDISPLAY:
1802 open_vumeter_window(this, inst);
1803 return;
1804 case wxID_DISASSEMBLER:
1805 wxeditor_disassembler_display(this, inst);
1806 return;
1807 case wxID_MOVIE_EDIT:
1808 wxeditor_movie_display(this, inst);
1809 return;
1810 case wxID_NEW_PROJECT:
1811 open_new_project_window(this, inst);
1812 return;
1813 case wxID_CLOSE_PROJECT:
1814 inst.iqueue->run([this]() -> void { this->inst.project->set(NULL); });
1815 return;
1816 case wxID_CLOSE_ROM:
1817 inst.iqueue->run([]() -> void { close_rom(); });
1818 return;
1819 case wxID_ENTER_FULLSCREEN:
1820 wx_escape_count = 0;
1821 enter_or_leave_fullscreen(true);
1822 return;
1823 case wxID_LOAD_ROM_IMAGE_FIRST:
1824 do_load_rom_image(NULL);
1825 return;
1826 case wxID_HEXEDITOR:
1827 wxeditor_hexedit_display(this, inst);
1828 return;
1829 case wxID_MULTITRACK:
1830 wxeditor_multitrack_display(this, inst);
1831 return;
1832 case wxID_CHDIR: {
1833 wxDirDialog* d = new wxDirDialog(this, wxT("Change working directory"), wxT("."),
1834 wxDD_DIR_MUST_EXIST);
1835 if(d->ShowModal() == wxID_CANCEL) {
1836 d->Destroy();
1837 return;
1839 std::string path = tostdstring(d->GetPath());
1840 d->Destroy();
1841 chdir(path.c_str());
1842 messages << "Changed working directory to '" << path << "'" << std::endl;
1843 return;
1845 case wxID_DOWNLOAD: {
1846 if(download_in_progress) return;
1847 filename = pick_text(this, "Download movie", "Enter URL to download");
1848 download_in_progress = new file_download();
1849 download_in_progress->url = lsnes_uri_rewrite(filename);
1850 download_in_progress->target_slot = "wxwidgets_download_tmp";
1851 download_in_progress->do_async(*inst.rom);
1852 new download_timer(this, inst);
1853 return;
1858 void wxwin_mainwindow::action_updated()
1860 runuifun([this]() { reinterpret_cast<system_menu*>(sysmenu)->update(true); });
1863 void wxwin_mainwindow::enter_or_leave_fullscreen(bool fs)
1865 CHECK_UI_THREAD;
1866 if(fs && !is_fs) {
1867 if(spanel_shown)
1868 toplevel->Detach(spanel);
1869 spanel->Hide();
1870 is_fs = fs;
1871 ShowFullScreen(true);
1872 becoming_fullscreen = true;
1873 request_paint(); //Finish the resizing by running paint handler.
1874 } else if(!fs && is_fs) {
1875 becoming_fullscreen = false;
1876 ShowFullScreen(false);
1877 gpanel->Show();
1878 if(spanel_shown) {
1879 spanel->Show();
1880 toplevel->Add(spanel, 1, wxGROW);
1882 Fit();
1883 gpanel->SetFocus();
1884 is_fs = fs;
1885 wx_escape_count = 0;
1886 request_paint(); //Don't leave graphical corruption.
1890 namespace
1892 struct command::stub _exit_fullscreen = {"exit-fullscreen", "Exit fullscreen",
1893 "Syntax: exit_fullscreen\nExit fullscreen"};
1894 command::fnptr<> exit_fullscreen(lsnes_cmds, _exit_fullscreen,
1895 []() throw(std::bad_alloc, std::runtime_error) {
1896 runuifun([]() {
1897 if(is_fs)
1898 main_window->enter_or_leave_fullscreen(false);