nestopia/source/fltkui/fltkui.cpp
2025-03-23 09:31:10 -06:00

944 lines
27 KiB
C++

/*
Copyright (c) 2012-2025 R. Danbrook
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <regex>
#include <set>
#include <epoxy/gl.h>
#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Menu_Bar.H>
#ifdef __APPLE__
#include <FL/Fl_Sys_Menu_Bar.H>
#endif
#include <FL/Fl_Native_File_Chooser.H>
#include <FL/Fl_PNG_Image.H>
#include <FL/Fl_Gl_Window.H>
#include <FL/Fl_Table_Row.H>
#include <FL/gl.h>
#include <SDL.h>
#include "audiomanager.h"
#include "chtmanager.h"
#include "inputmanager.h"
#include "jgmanager.h"
#include "setmanager.h"
#include "videomanager.h"
#include "fltkui.h"
#include "fltkui_archive.h"
#include "fltkui_cheats.h"
#include "fltkui_settings.h"
#include "logdriver.h"
#include "version.h"
namespace {
int paused{0};
int muted{0};
int speed{1};
int video_fullscreen{0};
int syncmode{0};
int refreshrate{60};
int screennum{0};
int frames{0};
int framefrags{0};
bool fdsgame{false};
NstWindow *nstwin{nullptr};
#ifdef __APPLE__
Fl_Sys_Menu_Bar *menubar{nullptr};
#else
Fl_Menu_Bar *menubar{nullptr};
#endif
NstGlArea *glarea{nullptr};
NstChtWindow *chtwin{nullptr};
NstSettingsWindow *setwin{nullptr};
JGManager *jgm{nullptr};
SettingManager *setmgr{nullptr};
InputManager *inputmgr{nullptr};
AudioManager *audiomgr{nullptr};
VideoManager *videomgr{nullptr};
CheatManager *chtmgr{nullptr};
std::vector<uint8_t> game;
Fl_Menu_Item menutable[] = {
{"&File", FL_ALT + 'f', 0, 0, FL_SUBMENU},
{"Open", 0, FltkUi::rom_open, 0, FL_MENU_DIVIDER},
{"Load State...", 0, FltkUi::state_load, 0, FL_MENU_INACTIVE},
{"Save State...", 0, FltkUi::state_save, 0, FL_MENU_DIVIDER|FL_MENU_INACTIVE},
{"Quick Load", 0, 0, 0, FL_SUBMENU|FL_MENU_INACTIVE},
{"Slot 0", 0, FltkUi::state_qload, (void*)"0", FL_MENU_INACTIVE},
{"Slot 1", 0, FltkUi::state_qload, (void*)"1", FL_MENU_INACTIVE},
{"Slot 2", 0, FltkUi::state_qload, (void*)"2", FL_MENU_INACTIVE},
{"Slot 3", 0, FltkUi::state_qload, (void*)"3", FL_MENU_INACTIVE},
{"Slot 4", 0, FltkUi::state_qload, (void*)"4", FL_MENU_INACTIVE},
{0},
{"Quick Save", 0, 0, 0, FL_SUBMENU|FL_MENU_DIVIDER|FL_MENU_INACTIVE},
{"Slot 0", 0, FltkUi::state_qsave, (void*)"0", FL_MENU_INACTIVE},
{"Slot 1", 0, FltkUi::state_qsave, (void*)"1", FL_MENU_INACTIVE},
{"Slot 2", 0, FltkUi::state_qsave, (void*)"2", FL_MENU_INACTIVE},
{"Slot 3", 0, FltkUi::state_qsave, (void*)"3", FL_MENU_INACTIVE},
{"Slot 4", 0, FltkUi::state_qsave, (void*)"4", FL_MENU_INACTIVE},
{0},
{"Open Palette...", 0, FltkUi::palette_open, 0, FL_MENU_DIVIDER},
{"Screenshot...", 0, FltkUi::screenshot_save, 0, FL_MENU_DIVIDER|FL_MENU_INACTIVE},
#ifndef __APPLE__
{"&Quit", FL_ALT + 'q', FltkUi::quit, 0, 0},
#endif
{0}, // End File
{"&Emulator", FL_ALT + 'e', 0, 0, FL_SUBMENU},
{"Pause", 0, FltkUi::pause, 0, FL_MENU_DIVIDER|FL_MENU_INACTIVE},
{"Mute", 0, FltkUi::mute, 0, FL_MENU_DIVIDER|FL_MENU_INACTIVE},
{"Reset (Soft)", 0, FltkUi::reset, (void*)"0", FL_MENU_INACTIVE},
{"Reset (Hard)", 0, FltkUi::reset, (void*)"1", FL_MENU_DIVIDER|FL_MENU_INACTIVE},
{"Fullscreen", 0, FltkUi::fullscreen, 0, FL_MENU_DIVIDER|FL_MENU_INACTIVE},
{"Switch Disk Side", 0, FltkUi::fds_next, 0, FL_MENU_INACTIVE},
{"Insert/Eject Disk", 0, FltkUi::fds_insert, 0, FL_MENU_DIVIDER|FL_MENU_INACTIVE},
{"Cheats...", 0, FltkUi::chtwin_open, 0, FL_MENU_DIVIDER|FL_MENU_INACTIVE},
{"Settings...", 0, FltkUi::setwin_open, 0, 0},
{0}, // End Emulator
#ifndef __APPLE__
{"&Help", FL_ALT + 'h', 0, 0, FL_SUBMENU},
{"About", 0, FltkUi::about, 0, 0},
{0}, // End Help
#endif
{0} // End Menu
};
Fl_Menu_Item *get_menuitem(std::string label) {
Fl_Menu_Item *m = menutable->first();
for (int i = 0; i < menutable->size(); ++i) {
if (m->label() != nullptr && std::string(m->label()) == label) {
return m;
}
++m;
}
return nullptr;
}
void update_refreshrate(void) {
// Get the screen refresh rate using an SDL window
if (!syncmode) { // Don't use this in "Timer" sync mode
return;
}
#ifdef __APPLE__
refreshrate = 120; // Dirty hack for modern macOS
return;
#endif
SDL_Window *sdlwin;
sdlwin = SDL_CreateWindow(
"refreshrate",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
1, 1, SDL_WINDOW_HIDDEN
);
SDL_DisplayMode dm;
SDL_GetCurrentDisplayMode(screennum, &dm);
refreshrate = dm.refresh_rate;
SDL_DestroyWindow(sdlwin);
}
void exec_emu_vsync(void*) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
inputmgr->event(event);
}
int fps{jgm->get_frametime()};
frames = (fps / refreshrate);
framefrags += fps % refreshrate;
if (framefrags >= refreshrate) {
frames++;
framefrags -= refreshrate;
}
if (!paused) {
for (int i = 0; i < frames * speed; i++) {
jgm->exec_frame();
}
}
glarea->redraw();
}
void exec_emu_timer(void*) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
inputmgr->event(event);
}
if (!paused) {
for (int i = 0; i < speed; i++) {
jgm->exec_frame();
}
}
Fl::repeat_timeout(1.0 / jgm->get_frametime(), exec_emu_timer);
glarea->redraw();
}
}
void FltkUi::chtwin_open(Fl_Widget *w, void *data) {
if (!jgm->is_loaded()) {
return;
}
chtwin->refresh();
chtwin->show();
}
void FltkUi::setwin_open(Fl_Widget *w, void *data) {
setwin->show();
}
void FltkUi::load_file(const char *filename) {
// First, check if it's an archive
std::set<std::string> archive_exts = { ".zip", ".7z", ".gz", ".bz2", ".xz", ".zst" };
std::string fileext = std::filesystem::path(filename).extension().string();
std::transform(fileext.begin(), fileext.end(), fileext.begin(),
[](unsigned char c) { return std::tolower(c); });
if (archive_exts.find(fileext) != archive_exts.end()) {
std::string arcname{};
if (fltkui_archive_select(filename, arcname)) {
if (arcname.empty()) {
return;
}
game.clear();
fltkui_archive_load_file(filename, arcname, game);
fileext = std::filesystem::path(arcname).extension().string();
jgm->load_game(arcname.c_str(), game);
}
else {
LogDriver::log(LogLevel::OSD, "No valid files in archive");
}
}
else {
std::ifstream stream(filename, std::ios::in | std::ios::binary);
if (!stream.is_open()) {
return;
}
game.clear();
game = std::vector<uint8_t>(std::istreambuf_iterator<char>(stream),
std::istreambuf_iterator<char>());
stream.close();
jgm->load_game(filename, game);
}
std::transform(fileext.begin(), fileext.end(), fileext.begin(),
[](unsigned char c) { return std::tolower(c); });
fdsgame = fileext == ".fds";
}
void FltkUi::rom_open(Fl_Widget *w, void *data) {
// Create native chooser
Fl_Native_File_Chooser fc;
fc.title("Select a ROM");
fc.type(Fl_Native_File_Chooser::BROWSE_FILE);
fc.filter("NES Games\t*.{nes,unf,fds,bin,zip,7z,gz,bz2,xz,xml,zst}");
run_emulation(false);
// Show file chooser
switch (fc.show()) {
case -1:
LogDriver::log(LogLevel::Error, std::string(fc.errmsg()));
break;
case 1: break; // Cancel
default:
if (fc.filename()) {
load_file(fc.filename());
if (jgm->is_loaded()) {
chtmgr->clear();
chtwin->refresh();
FltkUi::enable_menu();
nstwin->label(jgm->get_gamename().c_str());
jgm->setup_audio();
jgm->setup_video();
inputmgr->reassign();
audiomgr->pause(false);
jgm->reset(2);
}
}
break;
}
run_emulation();
}
void FltkUi::screenshot(std::string filename) {
if (filename.empty()) {
auto now = std::chrono::system_clock::now();
auto epoch = std::chrono::system_clock::from_time_t(0);
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>
(now.time_since_epoch()).count();
std::string msecs = std::to_string(duration);
filename = jgm->get_basepath() + "/screenshots/" + msecs + ".png";
}
videomgr->screenshot(filename);
LogDriver::log(LogLevel::OSD, "Screenshot Saved");
}
void FltkUi::screenshot_save(Fl_Widget *w, void *data) {
// Create native chooser
if (!jgm->is_loaded()) {
return;
}
run_emulation(false);
Fl_Native_File_Chooser fc;
fc.title("Save Screenshot");
fc.type(Fl_Native_File_Chooser::BROWSE_SAVE_FILE);
fc.filter("Screenshots\t*.png");
fc.options(Fl_Native_File_Chooser::SAVEAS_CONFIRM | Fl_Native_File_Chooser::USE_FILTER_EXT);
// Show file chooser
if (fc.show()) {
run_emulation();
return;
}
screenshot(fc.filename());
run_emulation();
}
void FltkUi::state_load(Fl_Widget *w, void *userdata) {
// Create native chooser
if (!jgm->is_loaded()) {
return;
}
run_emulation(false);
Fl_Native_File_Chooser fc;
fc.title("Load State");
fc.type(Fl_Native_File_Chooser::BROWSE_FILE);
//fc.directory((const char*)nstpaths.statepath);
fc.filter("Nestopia States\t*.nst");
// Show file chooser
switch (fc.show()) {
case -1:
LogDriver::log(LogLevel::Error, std::string(fc.errmsg()));
break;
case 1: break; // Cancel
default:
if (fc.filename()) {
std::string statefile{fc.filename()};
jgm->state_load(statefile);
}
break;
}
run_emulation();
}
void FltkUi::state_save(Fl_Widget *w, void *data) {
// Create native chooser
if (!jgm->is_loaded()) {
return;
}
run_emulation(false);
Fl_Native_File_Chooser fc;
fc.title("Save State");
fc.type(Fl_Native_File_Chooser::BROWSE_SAVE_FILE);
fc.filter("Nestopia States\t*.nst");
fc.options(Fl_Native_File_Chooser::SAVEAS_CONFIRM | Fl_Native_File_Chooser::USE_FILTER_EXT);
// Show file chooser
if (fc.show()) {
run_emulation();
return;
}
std::string statefile{fc.filename()};
jgm->state_save(statefile);
run_emulation();
}
void FltkUi::palette_open(Fl_Widget *w, void *data) {
// Create native chooser
Fl_Native_File_Chooser fc;
fc.title("Select a Palette");
fc.type(Fl_Native_File_Chooser::BROWSE_FILE);
fc.filter("NES Palettes\t*.pal");
// Show file chooser
switch (fc.show()) {
case -1:
LogDriver::log(LogLevel::Error, std::string(fc.errmsg()));
break;
case 1: break; // Cancel
default:
if (fc.filename()) {
// Overwrite the custom.pal file with the loaded one
std::filesystem::path src = fc.filename();
std::filesystem::path dst = jgm->get_basepath() + "/custom.pal";
std::filesystem::copy_file(src, dst, std::filesystem::copy_options::overwrite_existing);
// Force custom palette
jgm->get_setting("palette")->val = 12;
jgm->rehash();
setwin->set_choice_value("Emulator", "Palette", 12);
}
break;
}
}
void FltkUi::state_qload(Fl_Widget *w, void *data) {
int slot = atoi((const char*)data);
jgm->state_qload(slot);
}
void FltkUi::state_qsave(Fl_Widget *w, void *data) {
int slot = atoi((const char*)data);
jgm->state_qsave(slot);
}
void FltkUi::pause(Fl_Widget *w, void *data) {
Fl_Menu_Item* m = nullptr;
m = w ? const_cast<Fl_Menu_Item*>(((Fl_Menu_Bar*)w)->mvalue()) :
get_menuitem(paused ? "Play" : "Pause");
if (m == nullptr) {
LogDriver::log(LogLevel::Warn, "Menu item does not exist");
return;
}
paused ^= 1;
audiomgr->pause(paused);
m->label(paused ? "Play" : "Pause");
#ifdef __APPLE__
menubar->update();
#endif
}
void FltkUi::mute(Fl_Widget *w, void *data) {
Fl_Menu_Item* m = nullptr;
m = w ? const_cast<Fl_Menu_Item*>(((Fl_Menu_Bar*)w)->mvalue()) :
get_menuitem(muted ? "Unmute" : "Mute");
muted ^= 1;
audiomgr->mute(muted);
m->label(muted ? "Unmute" : "Mute");
#ifdef __APPLE__
menubar->update();
#endif
}
void FltkUi::reset(Fl_Widget *w, void *data) {
jgm->reset(atoi((const char*)data));
}
void NstWindow::resize(int x, int y, int w, int h) {
Fl_Double_Window::resize(x, y, w, h);
int nscreennum = Fl::screen_num(x, y, w, h);
if (nscreennum != screennum) { // Window moved to a different screen
screennum = nscreennum;
update_refreshrate();
}
videomgr->set_dpiscale(glarea->pixels_per_unit());
if (video_fullscreen) {
glarea->resize(0, 0, w, h);
videomgr->resize(w, h);
}
else {
glarea->resize(0, UI_MBARHEIGHT, w, h - UI_MBARHEIGHT);
videomgr->resize(w, h - UI_MBARHEIGHT);
}
}
void FltkUi::rehash() {
LogDriver::set_level(setmgr->get_setting("l_loglevel")->val);
videomgr->rehash(true);
audiomgr->rehash();
}
void FltkUi::fullscreen(Fl_Widget *w, void *data) {
if (!jgm->is_loaded()) {
return;
}
video_fullscreen ^= 1;
if (video_fullscreen) {
menubar->hide();
nstwin->fullscreen();
}
else {
menubar->show();
nstwin->fullscreen_off();
}
}
void FltkUi::fds_next(Fl_Widget *w, void *data) {
jgm->media_select();
}
void FltkUi::fds_insert(Fl_Widget *w, void *data) {
jgm->media_insert();
}
void FltkUi::about_close(Fl_Widget *w, void *data) {
Fl_Window *about = (Fl_Window*)data;
about->hide();
run_emulation();
}
void FltkUi::about(Fl_Widget *w, void *data) {
Fl_Window about(460, 440);
Fl_Box iconbox(166, 16, 128, 128);
Fl_Box text0(0, 144, 460, UI_SPACING, "Nestopia UE");
text0.labelfont(FL_BOLD);
Fl_Box text1(0, 166, 460, UI_SPACING, JG_VERSION);
Fl_Box text2(0, 208, 460, UI_SPACING, "Cycle-Accurate Nintendo Entertainment System Emulator");
Fl_Box text3(0, 256, 460, UI_SPACING,
"FLTK Frontend\n(c) 2012-2025, R. Danbrook\n\n"
"Portions derived from The Jolly Good Reference Frontend\n"
"(c) 2020-2025, Rupert Carmichael\n");
text3.labelsize(10);
Fl_Box text4(0, 320, 460, UI_SPACING,
"Nestopia Emulator\n"
"(c) 2020-2025, Rupert Carmichael\n"
"(c) 2012-2020, Nestopia UE Contributors\n"
"(c) 2003-2008, Martin Freij");
text4.labelsize(10);
Fl_Box text5(0, 360, 460, UI_SPACING, "Icon based on drawing by Trollekop");
text5.labelsize(10);
// Set up the icon
std::string iconpath{"icons/128/nestopia.png"};
if (!std::filesystem::exists(std::filesystem::path{iconpath})) {
iconpath = std::string(NST_DATAROOTDIR) + "/icons/hicolor/128x128/apps/nestopia.png";
}
Fl_PNG_Image nsticon(iconpath.c_str());
iconbox.image(nsticon);
Fl_Button close(360, 400, 80, UI_SPACING, "&Close");
close.shortcut(FL_ALT + 'c');
close.callback(FltkUi::about_close, (void*)&about);
about.set_modal();
run_emulation(false);
about.show();
while (about.shown()) {
Fl::wait();
}
}
void FltkUi::quit(Fl_Widget *w, void *data) {
videomgr->renderer_deinit();
nstwin->hide();
}
int FltkUi::handle(int e) {
// This is used to stop Esc from exiting the program
return (e == FL_SHORTCUT); // eat all keystrokes
}
int NstWindow::handle(int e) {
switch (e) {
case FL_KEYDOWN: case FL_KEYUP: {
#ifdef __APPLE__
inputmgr->event(Fl::event_key(), e == FL_KEYDOWN);
#else
// GNOME has a bad habit of spamming keyup events while holding
// a button down, so Fl::get_key is required here.
int key = Fl::event_key();
bool down = Fl::get_key(key);
inputmgr->event(key, down);
#endif
if (jgm->is_loaded()) {
inputmgr->ui_events();
}
break;
}
}
return Fl_Double_Window::handle(e);
}
void NstGlArea::draw() {
videomgr->render();
}
int NstGlArea::handle(int e) {
int xc, yc;
switch (e) {
case FL_ENTER:
if (inputmgr->get_lightgun()) {
cursor(setmgr->get_setting("m_hidecrosshair")->val ? FL_CURSOR_NONE : FL_CURSOR_CROSS);
}
else if (setmgr->get_setting("m_hidecursor")->val) {
cursor(FL_CURSOR_NONE);
}
break;
case FL_LEAVE:
cursor(FL_CURSOR_DEFAULT);
break;
case FL_PUSH:
inputmgr->event(Fl::event_button() + 1000, true);
return 1; // Must return 1 to receive drag events
case FL_RELEASE:
inputmgr->event(Fl::event_button() + 1000, false);
break;
case FL_MOVE:
videomgr->get_scaled_coords(Fl::event_x(), Fl::event_y(), &xc, &yc);
inputmgr->event(xc, yc);
break;
case FL_DRAG:
videomgr->get_scaled_coords(Fl::event_x(), Fl::event_y(), &xc, &yc);
inputmgr->event(xc, yc);
inputmgr->event(Fl::event_button() + 1000, Fl::event_state() ? true : false);
break;
case FL_DND_ENTER: // return 1 for these events to accept Drag and Drop
case FL_DND_DRAG:
case FL_DND_RELEASE:
return 1;
case FL_PASTE: { // handle the actual drop event
std::string filepath{std::regex_replace(std::string(Fl::event_text()),
std::regex("\\n|file://"), "")};
FltkUi::load_file(filepath.c_str());
if (jgm->is_loaded()) {
FltkUi::enable_menu();
nstwin->label(jgm->get_gamename().c_str());
jgm->setup_audio();
jgm->setup_video();
inputmgr->reassign();
audiomgr->pause(false);
jgm->reset(2);
// Restart if in timer sync mode
FltkUi::run_emulation(false);
FltkUi::run_emulation();
}
return 1;
}
}
return Fl_Gl_Window::handle(e);
}
void FltkUi::enable_menu() {
Fl_Menu_Item *mtable = (Fl_Menu_Item*)menubar->menu();
for (int i = 0; i < mtable[0].size(); ++i) {
if (!fdsgame && mtable[i].label() &&
std::string(mtable[i].label()).find("Disk") != std::string::npos) {
mtable[i].deactivate();
}
else {
mtable[i].activate();
}
}
#ifdef __APPLE__
menubar->update();
#endif
}
void FltkUi::show_inputmsg(int show) {
setwin->show_inputmsg(show);
}
void FltkUi::nstwin_open() {
int rw, rh;
videomgr->set_dimensions();
videomgr->get_dimensions(&rw, &rh);
Fl::add_handler(FltkUi::handle);
// Cheats Window
chtwin = new NstChtWindow(720, 500, "Cheat Manager", *chtmgr);
chtwin->populate();
// Settings Window
setwin = new NstSettingsWindow(500, 600, "Settings", *jgm, *setmgr, *inputmgr);
setwin->set_crt_active(setmgr->get_setting("v_postproc")->val == 3);
// Main Window
nstwin = new NstWindow(rw, rh + UI_MBARHEIGHT);
nstwin->color(FL_BLACK);
nstwin->xclass("nestopia");
#ifdef __APPLE__
// Apple style menu bar (top of screen)
menubar = new Fl_Sys_Menu_Bar(0, 0, nstwin->w(), UI_MBARHEIGHT);
// Set the "About" callback and "Window" menu style
Fl_Sys_Menu_Bar::about(about, nullptr);
Fl_Sys_Menu_Bar::window_menu_style(Fl_Sys_Menu_Bar::tabbing_mode_none);
#else
// Normal menu bar and window icon
menubar = new Fl_Menu_Bar(0, 0, nstwin->w(), UI_MBARHEIGHT);
// Set up the window icon
std::string iconpath{"icons/96/nestopia.png"};
if (!std::filesystem::exists(std::filesystem::path{iconpath})) {
iconpath = std::string(NST_DATAROOTDIR) + "/icons/hicolor/96x96/apps/nestopia.png";
}
Fl_PNG_Image nsticon(iconpath.c_str());
nstwin->default_icon(&nsticon);
#endif
menubar->box(FL_FLAT_BOX);
menubar->menu(menutable);
menubar->selection_color(NstGreen);
glarea = new NstGlArea(0, UI_MBARHEIGHT, nstwin->w(), nstwin->h() - UI_MBARHEIGHT);
nstwin->resizable(glarea);
glarea->color(FL_BLACK);
#ifdef __APPLE__
Fl::use_high_res_GL(1);
glarea->mode(FL_RGB | FL_RGB8 | FL_INDEX | FL_DOUBLE | FL_ACCUM |
FL_ALPHA | FL_DEPTH | FL_STENCIL |
(setmgr->get_setting("v_renderer")->val ? 0 : FL_OPENGL3));
#endif
glarea->end();
nstwin->end();
}
void FltkUi::set_ffspeed(bool on) {
if (on) {
speed = setmgr->get_setting("m_ffspeed")->val;
}
else {
speed = 1;
}
audiomgr->set_speed(speed);
}
void FltkUi::run_emulation(bool run) {
if (run) {
if (syncmode) {
Fl::add_idle(exec_emu_vsync);
}
else {
Fl::add_timeout(0.001, exec_emu_timer);
}
}
else {
if (syncmode) {
Fl::remove_idle(exec_emu_vsync);
}
else {
Fl::remove_timeout(exec_emu_timer);
}
}
}
int main(int argc, char *argv[]) {
// Parse command line arguments
std::string filename{};
std::vector<std::string> flags{};
for (int i = 1; i < argc; ++i) {
if (filename.empty() && argv[i][0] != '-') {
// The first non-flag argument is considered the filename
filename = std::string{argv[i]};
}
else if (argv[i][0] == '-') {
flags.push_back(std::string{argv[i]});
}
}
if (std::find(flags.begin(), flags.end(), "-v") != flags.end() ||
std::find(flags.begin(), flags.end(), "--version") != flags.end() ||
std::find(flags.begin(), flags.end(), "--help") != flags.end()) {
std::cout << "Nestopia UE " << JG_VERSION << std::endl;
return 0;
}
// Set default config options
setmgr = new SettingManager();
// Initialize SDL Audio and Joystick
#ifdef _WIN32
putenv("SDL_AUDIODRIVER=directsound");
#endif
if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_JOYSTICK) < 0) {
LogDriver::log(LogLevel::Error, "Failed to initialize SDL: " + std::string(SDL_GetError()));
return 1;
}
jgm = new JGManager();
// Read frontend and emulator settings
setmgr->read(*jgm);
LogDriver::set_level(setmgr->get_setting("l_loglevel")->val);
// Change "Mute" menubar label based on whether audio is muted at
// startup
Fl_Menu_Item *m = get_menuitem("Mute");
if (setmgr->get_setting("a_mute")->val) {
m->label("Unmute");
muted = 1;
}
// Bring up Audio/Video managers
audiomgr = new AudioManager(*jgm, *setmgr);
videomgr = new VideoManager(*jgm, *setmgr);
inputmgr = new InputManager(*jgm, *setmgr);
chtmgr = new CheatManager(*jgm);
// Load a rom from the command line
if (!filename.empty()) {
FltkUi::load_file(filename.c_str());
if (jgm->is_loaded()) {
jgm->setup_audio();
jgm->setup_video();
inputmgr->reassign();
video_fullscreen = setmgr->get_setting("v_fullscreen")->val ||
std::find(flags.begin(), flags.end(), "-f") != flags.end() ||
std::find(flags.begin(), flags.end(), "--fullscreen") != flags.end();
audiomgr->pause(false);
jgm->reset(2);
}
}
FltkUi::nstwin_open();
screennum = Fl::screen_num(nstwin->x_root(), nstwin->y_root());
if (jgm->is_loaded()) {
nstwin->label(jgm->get_gamename().c_str());
FltkUi::enable_menu();
}
else {
nstwin->label("Nestopia UE");
if (!filename.empty()) {
LogDriver::log(LogLevel::OSD, "Failed to load file from CLI");
}
}
nstwin->show();
menubar->show();
glarea->make_current();
glarea->show();
videomgr->renderer_init();
videomgr->set_dpiscale(glarea->pixels_per_unit());
videomgr->resize(glarea->w(), glarea->h());
if (video_fullscreen) {
video_fullscreen = 0;
FltkUi::fullscreen(NULL, NULL);
}
if (std::find(flags.begin(), flags.end(), "-m") != flags.end() ||
std::find(flags.begin(), flags.end(), "--mute") != flags.end()) {
FltkUi::mute();
}
syncmode = setmgr->get_setting("m_syncmode")->val;
LogDriver::log(LogLevel::Debug, syncmode ?
"Synchronization Mode: VSync" :
"Synchronization Mode: Timer");
update_refreshrate();
FltkUi::run_emulation();
while (nstwin->shown()) {
Fl::wait();
}
// Write frontend and emulator settings
setmgr->write(*jgm);
if (audiomgr) {
delete audiomgr;
}
if (videomgr) {
delete videomgr;
}
if (inputmgr) {
delete inputmgr;
}
if (jgm) {
delete jgm;
}
if (setmgr) {
delete setmgr;
}
if (chtmgr) {
delete chtmgr;
}
delete chtwin;
delete setwin;
delete glarea;
delete menubar;
delete nstwin;
return 0;
}