/*****************************************************************************\
     Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
                This file is licensed under the Snes9x License.
   For further information, consult the LICENSE file in the root directory.
\*****************************************************************************/

#include "gtk_compat.h"
#include "gtk_config.h"

#ifdef GDK_WINDOWING_X11
#include <X11/Xatom.h>
#endif

#ifdef USE_XV
#include <X11/extensions/XShm.h>
#include <X11/extensions/Xv.h>
#include <X11/extensions/Xvlib.h>
#endif

#include "gtk_shader_parameters.h"

#include "gtk_s9x.h"
#include "gtk_preferences.h"
#include "gtk_icon.h"
#include "gtk_display.h"
#include "gtk_file.h"
#include "gtk_sound.h"
#include "gtk_control.h"
#include "gtk_cheat.h"
#include "gtk_netplay.h"
#include "gtk_s9xwindow.h"

#include "fmt/format.h"

#include "snes9x.h"
#include "controls.h"
#include "movie.h"
#include "display.h"
#include "apu/apu.h"
#include "memmap.h"
#include "cpuexec.h"
#include "snapshot.h"
#include "netplay.h"

static Glib::RefPtr<Gtk::FileFilter> get_save_states_file_filter()
{
    const char *exts[] = { "*.sst", "*.000", "*.001", "*.002", "*.003", "*.004",
                           "*.005", "*.006", "*.007", "*.008", "*.009", nullptr };
    auto filter = Gtk::FileFilter::create();
    filter->set_name(_("Save States"));
    for (int i = 0; exts[i]; i++)
        filter->add_pattern(exts[i]);
    return filter;
}

static Glib::RefPtr<Gtk::FileFilter> get_all_files_filter()
{
    auto filter = Gtk::FileFilter::create();
    filter->set_name(_("All Files"));
    filter->add_pattern("*.*");
    filter->add_pattern("*");
    return filter;
}

Snes9xWindow::Snes9xWindow(Snes9xConfig *config)
    : GtkBuilderWindow("main_window")
{
    user_pause = 0;
    sys_pause = 0;
    last_width = -1;
    last_height = -1;
    this->config = config;
    recent_menu = nullptr;
    fullscreen_state = 0;
    maximized_state = 0;
    focused = true;
    paused_from_focus_loss = false;
    cr = nullptr;
    cairo_owned = false;
    mouse_grabbed = false;

    if (Gtk::IconTheme::get_default()->has_icon("snes9x"))
    {
        window->set_default_icon_name("snes9x");
    }
    else
    {
        extern int mini_icon_size;
        extern unsigned char mini_icon[];
        auto loader = Gdk::PixbufLoader::create();
        loader->write(mini_icon, mini_icon_size);
        loader->close();
        auto pixbuf = loader->get_pixbuf();
        if (pixbuf)
            window->set_default_icon(pixbuf);
    }

    drawing_area = get_object<Gtk::DrawingArea>("drawingarea").get();

    gtk_widget_realize(GTK_WIDGET(window->gobj()));
    gtk_widget_realize(GTK_WIDGET(drawing_area->gobj()));

    enable_widget("shader_parameters_item", false);

    connect_signals();

    if (config->window_width < 100 || config->window_height < 100)
    {
        config->window_width = 256;
        config->window_height = 224;
    }

    window->get_window()->set_cursor();

    resize(config->window_width, config->window_height);
}

void Snes9xWindow::connect_signals()
{
    drawing_area->signal_draw().connect(sigc::mem_fun(*this, &Snes9xWindow::draw));
    window->signal_delete_event().connect([](GdkEventAny *event) -> bool {
        S9xExit();
        return false;
    });
    get_object<Gtk::MenuItem>("exit_item")->signal_activate().connect([] {
         S9xExit();
    });

    window->signal_window_state_event().connect([&](GdkEventWindowState *state) -> bool {
        fullscreen_state = state->new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
        maximized_state = state->new_window_state & GDK_WINDOW_STATE_MAXIMIZED;
        return false;
    });

    window->signal_focus_in_event().connect([&](GdkEventFocus *event) -> bool {
        focus_notify(true);
        return false;
    });

    window->signal_focus_out_event().connect([&](GdkEventFocus *event) -> bool {
        focus_notify(false);
        return false;
    });

    last_key_pressed_keyval = 0;
    last_key_pressed_type = GDK_NOTHING;
    window->signal_key_press_event().connect(sigc::mem_fun(*this, &Snes9xWindow::event_key), false);
    window->signal_key_release_event().connect(sigc::mem_fun(*this, &Snes9xWindow::event_key), false);

    drawing_area->signal_button_press_event().connect(sigc::mem_fun(*this, &Snes9xWindow::button_press));
    drawing_area->signal_button_release_event().connect(sigc::mem_fun(*this, &Snes9xWindow::button_release));
    drawing_area->signal_motion_notify_event().connect(sigc::mem_fun(*this, &Snes9xWindow::motion_notify));

    get_object<Gtk::MenuItem>("continue_item")->signal_activate().connect([&] {
        unpause_from_user();
    });

    get_object<Gtk::MenuItem>("pause_item")->signal_activate().connect([&] {
        pause_from_user();
    });

    get_object<Gtk::MenuItem>("fullscreen_item")->signal_activate().connect([&] {
        toggle_fullscreen_mode();
    });

    get_object<Gtk::MenuItem>("open_rom_item")->signal_activate().connect([&] {
        open_rom_dialog();
    });

    get_object<Gtk::MenuItem>("reset_item")->signal_activate().connect([&] {
        S9xSoftReset();
    });

    get_object<Gtk::MenuItem>("hard_reset_item")->signal_activate().connect([&] {
        S9xReset();
    });

    get_object<Gtk::MenuItem>("shader_parameters_item")->signal_activate().connect([&] {
        gtk_shader_parameters_dialog(get_window());
    });

    const std::vector<const char *> port_items = { "joypad1", "mouse1", "superscope1", "joypad2", "mouse2", "multitap2", "superscope2", "nothingpluggedin2" };
    for (auto &name : port_items)
    {
        get_object<Gtk::MenuItem>(name)->signal_activate().connect(sigc::bind<const char *>(sigc::mem_fun(*this, &Snes9xWindow::port_activate), name));
    }

    for (int i = 0; i <= 9; i++)
    {
        std::string name = "load_state_" + std::to_string(i);
        get_object<Gtk::MenuItem>(name.c_str())->signal_activate().connect([i] {
            S9xQuickLoadSlot(i);
        });
        name = "save_state_" + std::to_string(i);
        get_object<Gtk::MenuItem>(name.c_str())->signal_activate().connect([i] {
            S9xQuickSaveSlot(i);
        });
    }

    get_object<Gtk::MenuItem>("from_file1")->signal_activate().connect([&] {
        load_state_dialog();
    });

    get_object<Gtk::MenuItem>("to_file1")->signal_activate().connect([&] {
        save_state_dialog();
    });

    get_object<Gtk::MenuItem>("load_state_undo")->signal_activate().connect([&] {
        S9xUnfreezeGame(S9xGetFilename(".undo", SNAPSHOT_DIR).c_str());
    });

    get_object<Gtk::MenuItem>("save_spc_item")->signal_activate().connect([&] {
        save_spc_dialog();
    });

    get_object<Gtk::MenuItem>("open_movie_item")->signal_activate().connect([&] {
        if (S9xMovieActive())
            S9xMovieStop(false);

        S9xMovieOpen(open_movie_dialog(true).c_str(), false);
    });

    get_object<Gtk::MenuItem>("stop_recording_item")->signal_activate().connect([&] {
        if (S9xMovieActive())
            S9xMovieStop(false);
    });

    get_object<Gtk::MenuItem>("jump_to_frame_item")->signal_activate().connect([&] {
        movie_seek_dialog();
    });

    get_object<Gtk::MenuItem>("record_movie_item")->signal_activate().connect([&] {
        if (S9xMovieActive())
            S9xMovieStop(false);

        S9xMovieCreate(open_movie_dialog(false).c_str(), 0xFF, MOVIE_OPT_FROM_RESET, nullptr, 0);
    });

    get_object<Gtk::MenuItem>("cheats_item")->signal_activate().connect([&] {
        open_snes9x_cheats_dialog();
    });

    get_object<Gtk::MenuItem>("preferences_item")->signal_activate().connect([&] {
        snes9x_preferences_open(this);
    });

    get_object<Gtk::MenuItem>("open_netplay_item")->signal_activate().connect([&] {
        S9xNetplayDialogOpen();
    });

    get_object<Gtk::MenuItem>("rom_info_item")->signal_activate().connect([&] {
        show_rom_info();
    });

    get_object<Gtk::MenuItem>("sync_clients_item")->signal_activate().connect([&] {
        S9xNetplaySyncClients();
    });

    get_object<Gtk::MenuItem>("hide_ui")->signal_activate().connect([&] {
        toggle_ui();
    });

    for (int i = 1; i <= 10; i++)
    {
        std::string name = "exact_pixels_" + std::to_string(i) + "x_item";
        get_object<Gtk::MenuItem>(name.c_str())->signal_activate().connect([i, this] {
            resize_to_multiple(i);
        });
    }

    get_object<Gtk::MenuItem>("open_multicart_item")->signal_activate().connect([&] {
        open_multicart_dialog();
    });
}

bool Snes9xWindow::button_press(GdkEventButton *event)
{
    if (S9xIsMousePluggedIn())
    {
        switch (event->button)
        {
        case 1:
            S9xReportButton(BINDING_MOUSE_BUTTON0, 1);
            break;
        case 2:
            S9xReportButton(BINDING_MOUSE_BUTTON2, 1);
            break;
        case 3:
            S9xReportButton(BINDING_MOUSE_BUTTON1, 1);
            break;
        }
    }
    else if (event->button == 3)
    {
        get_object<Gtk::Menu>("view_menu_menu")->popup_at_pointer(nullptr);
    }

    return false;
}

bool Snes9xWindow::button_release(GdkEventButton *event)
{
    switch (event->button)
    {
        case 1:
            S9xReportButton(BINDING_MOUSE_BUTTON0, 0);
            break;
        case 2:
            S9xReportButton(BINDING_MOUSE_BUTTON1, 0);
            break;
        case 3:
            S9xReportButton(BINDING_MOUSE_BUTTON2, 0);
            break;
    }

    return false;
}

bool Snes9xWindow::motion_notify(GdkEventMotion *event)
{
    if (!config->rom_loaded || last_width <= 0 || last_height <= 0)
        return false;

    if (mouse_grabbed)
    {
        if (event->x_root == gdk_mouse_x && event->y_root == gdk_mouse_y)
            return false;

        snes_mouse_x += (event->x_root - gdk_mouse_x);
        snes_mouse_y += (event->y_root - gdk_mouse_y);
        center_mouse();

        return false;
    }

    int scale_factor = window->get_scale_factor();

    snes_mouse_x = (uint16)((int)(event->x * scale_factor) - mouse_region_x) * 256 /
                   (mouse_region_width <= 0 ? 1 : mouse_region_width);

    snes_mouse_y = (uint16)((int)(event->y * scale_factor) - mouse_region_y) * (gui_config->overscan ? SNES_HEIGHT_EXTENDED : SNES_HEIGHT) /
                   (mouse_region_height <= 0 ? 1 : mouse_region_height);

    if (!config->pointer_is_visible)
    {
        if (!S9xIsMousePluggedIn())
            show_mouse_cursor();
    }

    config->pointer_timestamp = g_get_monotonic_time();

    return false;
}

void Snes9xWindow::port_activate(const char *name)
{
    auto item = get_object<Gtk::CheckMenuItem>(name);
    if (!item->get_active())
        return;

    if (!strcasecmp(name, "joypad1"))
    {
        S9xSetController(0, CTL_JOYPAD, 0, 0, 0, 0);
    }
    else if (!strcasecmp(name, "joypad2"))
    {
        S9xSetController(1, CTL_JOYPAD, 1, 0, 0, 0);
    }
    else if (!strcasecmp(name, "mouse1"))
    {
        S9xSetController(0, CTL_MOUSE, 0, 0, 0, 0);
    }
    else if (!strcasecmp(name, "mouse2"))
    {
        S9xSetController(1, CTL_MOUSE, 0, 0, 0, 0);
    }
    else if (!strcasecmp(name, "superscope1"))
    {
        S9xSetController(0, CTL_SUPERSCOPE, 0, 0, 0, 0);
    }
    else if (!strcasecmp(name, "superscope2"))
    {
        S9xSetController(1, CTL_SUPERSCOPE, 0, 0, 0, 0);
    }
    else if (!strcasecmp(name, "multitap1"))
    {
        S9xSetController(0, CTL_MP5, 0, 1, 2, 3);
    }
    else if (!strcasecmp(name, "multitap2"))
    {
        S9xSetController(1, CTL_MP5, 1, 2, 3, 4);
    }
    else if (!strcasecmp(name, "nothingpluggedin2"))
    {
        S9xSetController(1, CTL_NONE, 0, 0, 0, 0);
    }
}

bool Snes9xWindow::event_key(GdkEventKey *event)
{
    Binding b;
    s9xcommand_t cmd;

    // Ignore multiple identical keypresses to discard repeating keys
    if (event->keyval == last_key_pressed_keyval && event->type == last_key_pressed_type)
    {
        return true;
    }

    // Provide escape key to get out of fullscreen
    if (event->keyval == GDK_Escape)
    {
        if (event->type == GDK_KEY_RELEASE)
        {
            if (config->default_esc_behavior == ESC_EXIT_FULLSCREEN)
                leave_fullscreen_mode();
            else if (config->default_esc_behavior == ESC_EXIT_SNES9X)
                S9xExit();
            else
                toggle_ui();
        }

        return true;
    }

    last_key_pressed_keyval = event->keyval;
    last_key_pressed_type = event->type;

    b = Binding(event);

    // If no mapping for modifier version exists, try non-modifier
    cmd = S9xGetMapping(b.hex());
    if (cmd.type == S9xNoMapping)
    {
        b = Binding(event->keyval, false, false, false);
        cmd = S9xGetMapping(b.hex());
    }

    if (cmd.type != S9xNoMapping)
    {
        S9xReportButton(b.hex(), (event->type == GDK_KEY_PRESS));
        return true;
    }

    return false; // Pass the key to GTK
}

extern int gtk_splash_smtpe_size;
extern unsigned char gtk_splash_smtpe[];
extern int gtk_splash_combo_size;
extern unsigned char gtk_splash_combo[];
extern int gtk_splash_pattern_size;
extern unsigned char gtk_splash_pattern[];

void Snes9xWindow::setup_splash()
{
    uint16 *screen_ptr = GFX.Screen;

    // Load splash image (RGB24) into Snes9x buffer (RGB15)
    last_width = 256;
    last_height = 224;

    if (config->splash_image == SPLASH_IMAGE_PATTERN ||
        config->splash_image == SPLASH_IMAGE_SMTPE   ||
        config->splash_image == SPLASH_IMAGE_COMBO) {
        unsigned char *pattern = nullptr;
        int pattern_size = 0;

        if (config->splash_image == SPLASH_IMAGE_PATTERN) {
            pattern = gtk_splash_pattern;
            pattern_size = gtk_splash_pattern_size;
        } else if (config->splash_image == SPLASH_IMAGE_SMTPE) {
            pattern = gtk_splash_smtpe;
            pattern_size = gtk_splash_smtpe_size;
        } else {
            pattern = gtk_splash_combo;
            pattern_size = gtk_splash_combo_size;
        }

        auto pixbuf_loader = gdk_pixbuf_loader_new_with_type("png", nullptr);
        gdk_pixbuf_loader_write(pixbuf_loader, pattern, pattern_size, nullptr);
        gdk_pixbuf_loader_close(pixbuf_loader, nullptr);
        auto pixbuf = gdk_pixbuf_loader_get_pixbuf(pixbuf_loader);
        const unsigned char *splash_ptr = gdk_pixbuf_get_pixels(pixbuf);
        const int channels = gdk_pixbuf_get_n_channels(pixbuf);

        for (int y = 0; y < 224; y++, screen_ptr += (GFX.Pitch / 2)) {
            for (int x = 0; x < 256; x++) {
                unsigned int red = splash_ptr[0];
                unsigned int green = splash_ptr[1];
                unsigned int blue = splash_ptr[2];

                screen_ptr[x] = ((red & 0xF8) << 8) +
                                ((green & 0xF8) << 3) +
                                ((green & 0x80) >> 2) +
                                ((blue & 0xF8) >> 3);

                splash_ptr += channels;
            }
        }

        g_object_unref(pixbuf_loader);

        return;
    }

    if (config->splash_image == SPLASH_IMAGE_BLUE) {
        for (int y = 0; y < 224; y++, screen_ptr += (GFX.Pitch / 2)) {
            uint16 colora = (uint16)y / 7;
            uint16 colorb = ((uint16)y - 3) / 7;
            if (colorb > 32)
                colorb = 0;

            for (int x = 0; x < 256; x++) {
                screen_ptr[x] = ((x ^ y) & 1) ? colorb : colora;
            }
        }

        return;
    }
    return;

    for (int y = 0; y < 224; y++, screen_ptr += (GFX.Pitch / 2)) {
        memset(screen_ptr, 0, 256 * sizeof(uint16));
    }
}

bool Snes9xWindow::draw(const Cairo::RefPtr<Cairo::Context> &cr)
{
    this->cr = cr->cobj();
    cairo_owned = false;

    if (!(config->fullscreen) && !(maximized_state))
    {
        config->window_width = get_width();
        config->window_height = get_height();
    }

    if (last_width < 0)
    {
        setup_splash();
    }

    S9xDisplayRefresh();

    if (!(config->fullscreen))
    {
        config->window_width = get_width();
        config->window_height = get_height();
    }

    if ((is_paused() || NetPlay.Paused) && (gui_config->splash_image < SPLASH_IMAGE_STARFIELD || gui_config->rom_loaded))
    {
        S9xDeinitUpdate(last_width, last_height);
    }

    this->cr = nullptr;

    return true;
}

void Snes9xWindow::focus_notify(bool state)
{
    focused = state;

    if (!state && config->pause_emulation_on_switch)
    {
        sys_pause++;
        propagate_pause_state();
        paused_from_focus_loss = true;
    }

    if (state && paused_from_focus_loss)
    {
        unpause_from_focus_change();
        paused_from_focus_loss = false;
    }
}

void Snes9xWindow::open_multicart_dialog()
{
    int result;
    GtkBuilderWindow dialog_builder("multicart_dialog");
    auto dialog = Glib::RefPtr<Gtk::Dialog>::cast_static(dialog_builder.window);

    dialog->set_transient_for(*window.get());

    pause_from_focus_change();

    auto slota = get_object<Gtk::FileChooserDialog>("multicart_slota");
    auto slotb = get_object<Gtk::FileChooserDialog>("multicart_slotb");

    slota->set_current_folder(config->last_directory);
    slotb->set_current_folder(config->last_directory);

    result = dialog->run();
    dialog->hide();

    if (result == GTK_RESPONSE_OK)
    {
        auto filename = slota->get_filename();
        if (!filename.empty())
            strncpy(Settings.CartAName, filename.c_str(), PATH_MAX);
        else
            Settings.CartAName[0] = '\0';

        filename = slotb->get_filename();
        if (!filename.empty())
            strncpy(Settings.CartBName, filename.c_str(), PATH_MAX);
        else
            Settings.CartBName[0] = '\0';

        Settings.Multi = true;

        if (S9xOpenROM(nullptr))
        {
            auto msg = Gtk::MessageDialog(*window.get(),
                                          _("Couldn't load files."),
                                          false,
                                          Gtk::MESSAGE_ERROR,
                                          Gtk::BUTTONS_CLOSE,
                                          true);
            msg.run();
        }
    }

    unpause_from_focus_change();
}

std::string Snes9xWindow::open_movie_dialog(bool readonly)
{
    this->pause_from_focus_change();

    std::string title;
    Gtk::FileChooserAction action;

    if (readonly)
    {
        title = _("Open SNES Movie");
        action = Gtk::FILE_CHOOSER_ACTION_OPEN;
    }
    else
    {
        title = _("New SNES Movie");
        action = Gtk::FILE_CHOOSER_ACTION_SAVE;
    }

    Gtk::FileChooserDialog dialog(*window.get(), title, action);
    dialog.add_button(Gtk::StockID("gtk-cancel"), Gtk::RESPONSE_CANCEL);
    if (readonly)
        dialog.add_button(Gtk::StockID("gtk-open"), Gtk::RESPONSE_ACCEPT);
    else
        dialog.add_button(Gtk::StockID("gtk-save"), Gtk::RESPONSE_ACCEPT);

    if (!readonly)
    {
        auto default_name = S9xGetFilename(".smv", s9x_getdirtype::ROM_DIR);
        dialog.set_current_name(default_name);

    }

    auto filter = Gtk::FileFilter::create();
    filter->set_name(_("SNES Movies"));
    filter->add_pattern("*.smv");
    filter->add_pattern("*.SMV");
    dialog.add_filter(filter);
    dialog.add_filter(get_all_files_filter());

    dialog.set_current_folder(S9xGetDirectory(SRAM_DIR));
    auto result = dialog.run();
    dialog.hide();
    this->unpause_from_focus_change();

    if (result == Gtk::RESPONSE_ACCEPT)
        return dialog.get_filename();

    return std::string{};
}

std::string Snes9xWindow::open_rom_dialog(bool run)
{
    const char *extensions[] = {
        "*.smc", "*.SMC", "*.fig", "*.FIG", "*.sfc", "*.SFC",
        "*.jma", "*.JMA", "*.zip", "*.ZIP", "*.gd3", "*.GD3",
        "*.swc", "*.SWC", "*.gz", "*.GZ", "*.bs", "*.BS",
        NULL
    };

    pause_from_focus_change();

    auto dialog = Gtk::FileChooserDialog(*top_level->window.get(),
                                         _("Open SNES ROM Image"));
    dialog.add_button(Gtk::StockID("gtk-cancel"), Gtk::RESPONSE_CANCEL);
    dialog.add_button(Gtk::StockID("gtk-open"), Gtk::RESPONSE_ACCEPT);

    auto filter = Gtk::FileFilter::create();
    filter->set_name(_("SNES ROM Images"));
    for (int i = 0; extensions[i]; i++)
        filter->add_pattern(extensions[i]);
    dialog.add_filter(filter);
    dialog.add_filter(get_all_files_filter());

    if (!gui_config->last_directory.empty())
        dialog.set_current_folder(config->last_directory);

    auto result = dialog.run();
    dialog.hide();

    std::string filename;

    if (result == Gtk::RESPONSE_ACCEPT)
    {
        std::string directory = dialog.get_current_folder();

        if (!directory.empty())
            gui_config->last_directory = directory;

        filename = dialog.get_filename();

        if (!filename.empty())
        {
            if (run)
            {
                Settings.Multi = false;
                try_open_rom(filename);
            }
        }
    }

    unpause_from_focus_change();
    return filename;
}

bool Snes9xWindow::try_open_rom(std::string filename)
{
    pause_from_focus_change();

    Settings.Multi = false;

    if (S9xOpenROM(filename.c_str()))
    {
        std::string message = _("Couldn't load file: ") + filename;
        Gtk::MessageDialog msg(*window.get(), message, false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_CLOSE, true);
        msg.run();
        unpause_from_focus_change();
        return false;
    }

    Gtk::RecentManager::Data data;
    data.description = "SNES ROM";
    data.mime_type = "application/x-snes-rom";
    data.app_name = "Snes9x";
    data.groups = { "cartridge" };
    data.is_private = false;
    data.app_exec = Glib::get_prgname() + " %f";
    auto uri = Glib::filename_to_uri(filename);
    Gtk::RecentManager::get_default()->add_item(uri, data);

    unpause_from_user();
    unpause_from_focus_change();

    return true;
}

void Snes9xWindow::load_state_dialog()
{
    this->pause_from_focus_change();

    Gtk::FileChooserDialog dialog(*window.get(), _("Load Saved State"));
    dialog.add_button(Gtk::StockID("gtk-cancel"), Gtk::RESPONSE_CANCEL);
    dialog.add_button(Gtk::StockID("gtk-open"), Gtk::RESPONSE_ACCEPT);

    dialog.add_filter(get_save_states_file_filter());
    dialog.add_filter(get_all_files_filter());
    dialog.set_current_folder(S9xGetDirectory(SNAPSHOT_DIR));

    auto result = dialog.run();
    dialog.hide();
    if (result == Gtk::RESPONSE_ACCEPT)
    {
        S9xLoadState(dialog.get_filename());
    }

    unpause_from_focus_change();
}

void Snes9xWindow::movie_seek_dialog()
{
    if (!S9xMovieActive())
        return;

    pause_from_focus_change();

    GtkBuilderWindow seek_dialog("frame_advance_dialog");

    {
        std::string str;
        str = fmt::format(_("The current frame in the movie is <b>{0:Ld}</b>."), S9xMovieGetFrameCounter());
        seek_dialog.get_object<Gtk::Label>("current_frame_label")->set_label(str);

        str = fmt::format("{0:d}", S9xMovieGetFrameCounter());
        seek_dialog.set_entry_text("frame_entry", str);
    }

    auto dialog = Glib::RefPtr<Gtk::Dialog>::cast_static(seek_dialog.window);

    dialog->set_transient_for(*window.get());
    auto result = dialog->run();

    int entry_value = seek_dialog.get_entry_value("frame_entry");

    switch (result)
    {
    case Gtk::RESPONSE_OK:
        if (entry_value > 0 &&
            entry_value > (int)S9xMovieGetFrameCounter())
        {
            Settings.HighSpeedSeek =
                entry_value - S9xMovieGetFrameCounter();
        }

        break;
    }

    unpause_from_focus_change();
}

void Snes9xWindow::save_state_dialog()
{
    pause_from_focus_change();

    auto dialog = Gtk::FileChooserDialog(*window.get(), _("Save State"), Gtk::FILE_CHOOSER_ACTION_SAVE);
    dialog.add_button(Gtk::StockID("gtk-cancel"), Gtk::RESPONSE_CANCEL);
    dialog.add_button(Gtk::StockID("gtk-save"), Gtk::RESPONSE_ACCEPT);
    dialog.set_current_folder(S9xGetDirectory(SNAPSHOT_DIR));
    dialog.set_current_name(S9xBasename(S9xGetFilename(".sst", SNAPSHOT_DIR)));
    dialog.add_filter(get_save_states_file_filter());
    dialog.add_filter(get_all_files_filter());

    auto result = dialog.run();
    dialog.hide();

    if (result == GTK_RESPONSE_ACCEPT)
        S9xSaveState(dialog.get_filename());

    unpause_from_focus_change();
}

void Snes9xWindow::save_spc_dialog()
{
    pause_from_focus_change();

    auto dialog = Gtk::FileChooserDialog(*window.get(), _("Save SPC file..."), Gtk::FILE_CHOOSER_ACTION_SAVE);
    dialog.add_button(Gtk::StockID("gtk-cancel"), Gtk::RESPONSE_CANCEL);
    dialog.add_button(Gtk::StockID("gtk-save"), Gtk::RESPONSE_ACCEPT);
    dialog.set_current_folder(S9xGetDirectory(SNAPSHOT_DIR));
    dialog.set_current_name(S9xGetFilename(".spc", SNAPSHOT_DIR));
    auto filter = Gtk::FileFilter::create();
    filter->add_pattern("*.spc");
    filter->add_pattern("*.SPC");
    dialog.add_filter(filter);
    dialog.add_filter(get_all_files_filter());

    auto result = dialog.run();
    dialog.hide();

    if (result == Gtk::RESPONSE_ACCEPT)
    {
        if (S9xSPCDump(dialog.get_filename().c_str()))
        {
            /* Success ? */
        }
        else
        {
            std::string message = _("Couldn't save SPC file:");
            message += " " + dialog.get_filename();
            Gtk::MessageDialog(*window.get(), message, false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_CLOSE, true).run();
        }
    }

    unpause_from_focus_change();
}

void Snes9xWindow::set_menu_item_selected(const char *name)
{
    get_object<Gtk::CheckMenuItem>(name)->set_active(true);
}

void Snes9xWindow::show_rom_info()
{
const char *markup = _(R"(<b>Information for %s</b>

<i>Name:</i> %s
<i>Speed:</i> %02X/%s
<i>Map:</i> %s
<i>Type:</i> %02x
<i>Contents:</i> %s
<i>ROM Size:</i> %s
<i>Calculated Size:</i> %d
<i>SRAM Size:</i> %s
<i>Header Checksum:</i> %04X
<i>Checksum Complement:</i> %04X
<i>Actual Checksum:</i> %04X
<i>Video:</i> %s
<i>CRC32:</i> %08X
<i>Revision:</i> %s
<b><i>%s%s</i></b>)");
    char output[2048];

    snprintf(output, 2048, markup,
             Memory.ROMFilename.c_str(),
             Glib::convert(Memory.ROMName, "utf8", "shift-jis").c_str(),
             Memory.ROMSpeed,
             ((Memory.ROMSpeed & 0x10) != 0) ? "FastROM" : "SlowROM",
             (Memory.HiROM) ? "HiROM" : "LoROM",
             Memory.ROMType,
             Memory.KartContents(),
             Memory.Size(),
             Memory.CalculatedSize / 0x20000,
             Memory.StaticRAMSize(),
             Memory.ROMChecksum,
             Memory.ROMComplementChecksum,
             Memory.CalculatedChecksum,
             (Memory.ROMRegion > 12 || Memory.ROMRegion < 2) ? "NTSC 60Hz" : "PAL 50Hz",
             Memory.ROMCRC32,
             Memory.Revision(),
             (Settings.IsPatched) ? _("\n\nThis ROM has been auto-patched with ") : (Memory.ROMChecksum != Memory.CalculatedChecksum) ? _("\n\nThis ROM has been modified or damaged") : "",
             Settings.IsPatched == 1 ? "IPS" : Settings.IsPatched == 2 ? "BPS" : Settings.IsPatched == 3 ? "UPS" : "");

    std::string str_output(output);

    for (size_t i = 0; i < str_output.length(); i++)
    {
        if (str_output[i] == '&')
            str_output.insert(i + 1, "amp;");
    }

    pause_from_focus_change();

    auto dialog = Gtk::MessageDialog(*window.get(), str_output, true, Gtk::MESSAGE_OTHER, Gtk::BUTTONS_CLOSE, true);
    dialog.set_title(_("File Information"));
    dialog.run();

    unpause_from_focus_change();
}

void Snes9xWindow::configure_widgets()
{
    enable_widget("continue_item", config->rom_loaded);
    enable_widget("pause_item", config->rom_loaded);
    enable_widget("reset_item", config->rom_loaded);
    enable_widget("load_state_item", config->rom_loaded);
    enable_widget("save_state_item", config->rom_loaded);
    enable_widget("save_spc_item", config->rom_loaded);
    enable_widget("hard_reset_item", config->rom_loaded);
    enable_widget("record_movie_item", config->rom_loaded);
    enable_widget("stop_recording_item", config->rom_loaded);
    enable_widget("open_movie_item", config->rom_loaded);
    enable_widget("jump_to_frame_item", config->rom_loaded);
    enable_widget("cheats_item", config->rom_loaded);
    enable_widget("rom_info_item", config->rom_loaded);

    enable_widget("sync_clients_item",
                  config->rom_loaded &&
                      Settings.NetPlay &&
                      Settings.NetPlayServer);

    if (config->default_esc_behavior != ESC_TOGGLE_MENUBAR)
    {
        enable_widget("fullscreen_item", config->rom_loaded);

        config->ui_visible = true;

        show_widget("menubar", !config->fullscreen);
        show_widget("hide_ui", false);
        show_widget("hide_ui_separator", false);
    }
    else
    {
        enable_widget("fullscreen_item", true);

        show_widget("hide_ui", true);
        show_widget("hide_ui_separator", true);
        show_widget("menubar", config->ui_visible);
    }

    propagate_pause_state();

    if (config->rom_loaded && !Settings.Paused)
        hide_mouse_cursor();
    else
        show_mouse_cursor();

    if (config->rom_loaded)
    {
        std::string title = S9xBasenameNoExt(Memory.ROMFilename);
        title += " - Snes9x";
        window->set_title(title);
    }
    else
    {
        window->set_title("Snes9x");
    }
}

void Snes9xWindow::set_mouseable_area(int x, int y, int width, int height)
{
    mouse_region_x = x;
    mouse_region_y = y;
    mouse_region_width = width;
    mouse_region_height = height;
}

void Snes9xWindow::reset_screensaver()
{
    if (!focused)
        return;

    GdkWindow *gdk_window = window->get_window()->gobj();
    GdkDisplay *gdk_display = window->get_display()->gobj();

#ifdef GDK_WINDOWING_X11
    if (GDK_IS_X11_WINDOW(gdk_window))
    {
        XResetScreenSaver(GDK_DISPLAY_XDISPLAY(gdk_display));
    }
#endif

    config->screensaver_needs_reset = false;
}

void Snes9xWindow::toggle_fullscreen_mode()
{
    if (config->fullscreen)
        leave_fullscreen_mode();
    else
        enter_fullscreen_mode();
}

static double XRRGetExactRefreshRate(Display *dpy, Window window)
{
    XRRScreenResources *resources = nullptr;
    XRRCrtcInfo *crtc_info = nullptr;
    int event_base;
    int error_base;
    int version_major;
    int version_minor;
    double refresh_rate = 0.0;
    int i;

    if (!XRRQueryExtension(dpy, &event_base, &error_base) ||
        !XRRQueryVersion(dpy, &version_major, &version_minor))
    {
        return refresh_rate;
    }

    if (version_minor < 3)
        return refresh_rate;

    resources = XRRGetScreenResourcesCurrent(dpy, window);
    crtc_info = XRRGetCrtcInfo(dpy, resources, resources->crtcs[0]);

    for (i = 0; i < resources->nmode; i++)
    {
        if (resources->modes[i].id == crtc_info->mode)
        {
            XRRModeInfo *m = &resources->modes[i];

            refresh_rate = (double)m->dotClock / m->hTotal / m->vTotal;
            refresh_rate /= m->modeFlags & RR_DoubleScan ? 2 : 1;
            refresh_rate /= m->modeFlags & RR_ClockDivideBy2 ? 2 : 1;
            refresh_rate *= m->modeFlags & RR_DoubleClock ? 2 : 1;

            break;
        }
    }

    XRRFreeCrtcInfo(crtc_info);
    XRRFreeScreenResources(resources);

    return refresh_rate;
}

double
Snes9xWindow::get_refresh_rate()
{
    double refresh_rate = 0.0;
#if defined GDK_WINDOWING_X11 || defined GDK_WINDOWING_WAYLAND
    GdkDisplay *gdk_display = window->get_display()->gobj();
    GdkWindow *gdk_window = window->get_window()->gobj();
#endif

#ifdef GDK_WINDOWING_X11
    if (GDK_IS_X11_DISPLAY(gdk_display))
    {
        Window xid = gdk_x11_window_get_xid(gdk_window);
        Display *dpy = gdk_x11_display_get_xdisplay(gdk_display);
        refresh_rate = XRRGetExactRefreshRate(dpy, xid);
    }
#endif

#ifdef GDK_WINDOWING_WAYLAND
    if (GDK_IS_WAYLAND_DISPLAY(gdk_display))
    {
        GdkMonitor *monitor = gdk_display_get_monitor_at_window(gdk_display, gdk_window);
        refresh_rate = (double)gdk_monitor_get_refresh_rate(monitor) / 1000.0;
    }
#endif

    if (refresh_rate < 10.0)
    {
        printf("Warning: Couldn't read refresh rate.\n");
        refresh_rate = 60.0;
    }

    return refresh_rate;
}

int Snes9xWindow::get_auto_input_rate()
{
    double refresh_rate = get_refresh_rate();

    if (refresh_rate == 0.0)
        return 0;

    // Try for a close multiple of 60hz
    if (refresh_rate > 119.0 && refresh_rate < 121.0)
        refresh_rate /= 2.0;
    if (refresh_rate > 179.0 && refresh_rate < 181.0)
        refresh_rate /= 3.0;
    if (refresh_rate > 239.0 && refresh_rate < 241.0)
        refresh_rate /= 4.0;

    double new_input_rate = refresh_rate * 32040.0 / 60.09881389744051 + 0.5;

    if (new_input_rate > 32040.0 * 1.05 || new_input_rate < 32040.0 * 0.95)
        new_input_rate = 0.0;

    return new_input_rate;
}

#ifdef GDK_WINDOWING_X11
static void set_bypass_compositor(Display *dpy, Window window, unsigned char bypass)
{
    uint32 value = bypass;
    Atom net_wm_bypass_compositor = XInternAtom(dpy, "_NET_WM_BYPASS_COMPOSITOR", False);
    XChangeProperty(dpy, window, net_wm_bypass_compositor, XA_CARDINAL, 32, PropModeReplace, (const unsigned char *)&value, 1);
}
#endif

void Snes9xWindow::enter_fullscreen_mode()
{
    int rom_loaded = config->rom_loaded;

    if (config->fullscreen)
        return;

    GdkDisplay *gdk_display = window->get_display()->gobj();
    GdkWindow *gdk_window = window->get_window()->gobj();

    config->rom_loaded = 0;

    nfs_width = config->window_width;
    nfs_height = config->window_height;
    int nfs_x;
    int nfs_y;
    window->get_position(nfs_x, nfs_y);

#ifdef GDK_WINDOWING_X11
    if (config->change_display_resolution && GDK_IS_X11_WINDOW(gdk_window))
    {
        Display *dpy = gdk_x11_display_get_xdisplay(gdk_display);

        auto xrr_screen_resources = XRRGetScreenResourcesCurrent(dpy, gdk_x11_window_get_xid(gdk_window));
        auto xrr_crtc_info = XRRGetCrtcInfo(dpy,
                                            xrr_screen_resources,
                                            xrr_screen_resources->crtcs[0]);


        gdk_display_sync(gdk_display);
        if (XRRSetCrtcConfig(dpy,
                             xrr_screen_resources,
                             xrr_screen_resources->crtcs[0],
                             CurrentTime,
                             xrr_crtc_info->x,
                             xrr_crtc_info->y,
                             xrr_screen_resources->modes[config->xrr_index].id,
                             xrr_crtc_info->rotation,
                             &xrr_crtc_info->outputs[0],
                             1) != 0)
        {
            config->change_display_resolution = 0;
        }
        XRRFreeCrtcInfo(xrr_crtc_info);
        XRRFreeScreenResources(xrr_screen_resources);

        if (gui_config->auto_input_rate)
        {
            Settings.SoundInputRate = top_level->get_auto_input_rate();
            S9xUpdateDynamicRate(1, 2);
        }
    }
#endif

    /* Make sure everything is done synchronously */
    gdk_display_sync(gdk_display);
    window->fullscreen();
    gdk_display_sync(gdk_display);
    window->present();

    if (config->auto_vrr)
    {
        autovrr_saved_frameskip = Settings.SkipFrames;
        autovrr_saved_sound_input_rate = Settings.SoundInputRate;
        autovrr_saved_sync_to_vblank = gui_config->sync_to_vblank;
        autovrr_saved_sound_sync = Settings.SoundSync;

        Settings.SoundSync = false;
        Settings.SkipFrames = THROTTLE_TIMER;
        Settings.SoundInputRate = 32040;
        S9xUpdateDynamicRate(1, 2);
        gui_config->sync_to_vblank = true;
        S9xDisplayRefresh();
    }

#ifdef GDK_WINDOWING_X11
    if (GDK_IS_X11_WINDOW(window->get_window()->gobj()) &&
        config->default_esc_behavior != ESC_TOGGLE_MENUBAR)
    {
        set_bypass_compositor(gdk_x11_display_get_xdisplay(gdk_display),
                              gdk_x11_window_get_xid(gdk_window),
                              1);
    }
#endif
    config->fullscreen = 1;
    config->rom_loaded = rom_loaded;


    /* If we're running a game, disable ui when entering fullscreen */
    if (!Settings.Paused && config->rom_loaded)
        config->ui_visible = false;

    configure_widgets();
}

void Snes9xWindow::leave_fullscreen_mode()
{
    int rom_loaded = config->rom_loaded;

    if (!config->fullscreen)
        return;

    if (config->auto_vrr)
    {
        Settings.SkipFrames = autovrr_saved_frameskip;
        Settings.SoundInputRate = autovrr_saved_sound_input_rate;
        gui_config->sync_to_vblank = autovrr_saved_sync_to_vblank;
        Settings.SoundSync = autovrr_saved_sound_sync;
        S9xUpdateDynamicRate(1, 2);
        S9xDisplayRefresh();
    }

    GdkDisplay *gdk_display = window->get_display()->gobj();
    GdkWindow *gdk_window = window->get_window()->gobj();

    config->rom_loaded = 0;

#ifdef GDK_WINDOWING_X11
    if (config->change_display_resolution && GDK_IS_X11_WINDOW(gdk_window))
    {
        Display *dpy = gdk_x11_display_get_xdisplay(gdk_display);

        if (config->xrr_index > config->xrr_screen_resources->nmode)
            config->xrr_index = 0;

        gdk_display_sync(gdk_display);
        XRRSetCrtcConfig(dpy,
                         config->xrr_screen_resources,
                         config->xrr_screen_resources->crtcs[0],
                         CurrentTime,
                         config->xrr_crtc_info->x,
                         config->xrr_crtc_info->y,
                         config->xrr_crtc_info->mode,
                         config->xrr_crtc_info->rotation,
                         &config->xrr_crtc_info->outputs[0],
                         1);

        if (gui_config->auto_input_rate)
        {
            Settings.SoundInputRate = top_level->get_auto_input_rate();
            S9xUpdateDynamicRate(1, 2);
        }
    }
#endif

#ifdef GDK_WINDOWING_WAYLAND
    if (GDK_IS_WAYLAND_WINDOW(gdk_window))
    {
        S9xDeinitDisplay();
    }
#endif

    window->unfullscreen();

    window->show();

#ifdef GDK_WINDOWING_X11
    if (GDK_IS_X11_WINDOW(gdk_window))
    {
        set_bypass_compositor(gdk_x11_display_get_xdisplay(gdk_display),
                              gdk_x11_window_get_xid(gdk_window),
                              0);
    }
#endif

    resize(nfs_width, nfs_height);
    window->move(nfs_x, nfs_y);

    config->rom_loaded = rom_loaded;
    config->fullscreen = 0;
    configure_widgets();
    window->show();

#ifdef GDK_WINDOWING_WAYLAND
    if (GDK_IS_WAYLAND_WINDOW(gdk_window))
    {
        S9xReinitDisplay();
    }
#endif

}

void Snes9xWindow::resize_viewport(int width, int height)
{
    auto menubar = get_object<Gtk::MenuBar>("menubar");
    if (menubar->get_visible())
        height += menubar->get_height();

    resize(width, height);
}

void Snes9xWindow::hide_mouse_cursor()
{
    auto blank = Gdk::Cursor::create(Gdk::BLANK_CURSOR);
    drawing_area->get_window()->set_cursor(blank);
    config->pointer_is_visible = false;
}

void Snes9xWindow::show_mouse_cursor()
{
    auto left_ptr = Gdk::Cursor::create(Gdk::LEFT_PTR);
    drawing_area->get_window()->set_cursor(left_ptr);
    config->pointer_is_visible = true;
}

void Snes9xWindow::center_mouse()
{
    int x;
    int y;

    window->get_window()->get_origin(x, y);
    int w = window->get_width();
    int h = window->get_height();

    gdk_mouse_x = x + w / 2;
    gdk_mouse_y = y + h / 2;

    window->get_display()->get_default_seat()->get_pointer()->warp(window->get_screen(), gdk_mouse_x, gdk_mouse_y);
}

void Snes9xWindow::toggle_grab_mouse()
{
    if ((!mouse_grabbed && !S9xIsMousePluggedIn()) || !config->rom_loaded)
        return;

    auto seat = window->get_display()->get_default_seat();
    if (!mouse_grabbed)
        seat->grab(window->get_window(), Gdk::SEAT_CAPABILITY_ALL_POINTING, true, Gdk::Cursor::create(Gdk::BLANK_CURSOR));
    else
        seat->ungrab();

    S9xReportPointer(BINDING_MOUSE_POINTER, 0, 0);
    snes_mouse_x = 0.0;
    snes_mouse_y = 0.0;
    mouse_grabbed = !mouse_grabbed;
    if (mouse_grabbed)
        center_mouse();
}

void Snes9xWindow::show()
{
    window->show();
    configure_widgets();

    if (!recent_menu)
    {
        auto manager = Gtk::RecentManager::get_default();
        recent_menu = new Gtk::RecentChooserMenu(manager);

        auto filter = Gtk::RecentFilter::create();
        filter->add_group("cartridge");
        recent_menu->add_filter(filter);
        recent_menu->set_local_only();
        recent_menu->set_show_icons(false);
        recent_menu->set_sort_type(Gtk::RECENT_SORT_MRU);
        get_object<Gtk::MenuItem>("open_recent_item")->set_submenu(*recent_menu);
        recent_menu->signal_item_activated().connect([&] {
            try_open_rom(Glib::filename_from_uri(recent_menu->get_current_uri()).c_str());
        });

        recent_menu->show();

        auto clear_recent = get_object<Gtk::MenuItem>("clear_recent_items");
        clear_recent->signal_activate().connect([&] {
            auto manager = Gtk::RecentManager::get_default();
            auto items = manager->get_items();
            for (auto &i : items)
            {
                auto groups = i->get_groups();
                if (groups.end() != std::find_if(groups.begin(), groups.end(), [](Glib::ustring &name) -> bool { return name == "cartridge"; }))
                    manager->remove_item(i->get_uri());
            }
        });
    }
}

void Snes9xWindow::propagate_pause_state()
{
    int oldpause = Settings.Paused;

    Settings.Paused = (sys_pause || user_pause || !(config->rom_loaded));

    if (Settings.Paused != oldpause)
    {
        if (!is_paused())
        {
            S9xSoundStart();
            if (config->rom_loaded)
                enable_widget("pause_item", true);
        }
        else
        {
            S9xSoundStop();
            enable_widget("pause_item", false);
        }

        configure_widgets();
    }
}

void Snes9xWindow::toggle_ui()
{
    config->ui_visible = !config->ui_visible;
    configure_widgets();
}

/* gui_[un]pause Handles when system needs to pause the emulator */
void Snes9xWindow::pause_from_focus_change()
{
    sys_pause += config->modal_dialogs;
    propagate_pause_state();
}

void Snes9xWindow::unpause_from_focus_change()
{
    if (--sys_pause < 0)
        sys_pause = 0;
    propagate_pause_state();
}

/* client_[un]pause Handles when user manually chooses to pause */
void Snes9xWindow::pause_from_user()
{
    user_pause = true;
    propagate_pause_state();
}

void Snes9xWindow::unpause_from_user()
{
    user_pause = false;
    propagate_pause_state();
}

bool Snes9xWindow::is_paused()
{
    if (user_pause || sys_pause || Settings.Paused || !(config->rom_loaded))
        return true;

    return false;
}

void Snes9xWindow::set_accelerator_to_binding(const char *name, const char *binding)
{
    Binding bin;

    if (!strcmp(binding, "Escape Key"))
    {
        bin = Binding(GDK_Escape, false, false, false);
    }
    else
    {
        bin = S9xGetBindingByName(binding);
    }

    if (!(bin.is_key()))
        return;

    AcceleratorEntry entry{};
    entry.name = name;
    entry.key = bin.get_key();
    entry.modifiers = bin.get_gdk_modifiers();

    get_object<Gtk::MenuItem>(name)->add_accelerator("activate", accel_group, entry.key, entry.modifiers, Gtk::ACCEL_VISIBLE);
    accelerators.push_back(entry);
}

void Snes9xWindow::update_accelerators()
{
    if (!accel_group)
    {
        accel_group = Gtk::AccelGroup::create();
        window->add_accel_group(accel_group);
    }

    for (auto &entry : accelerators)
    {
        get_object<Gtk::MenuItem>(entry.name.c_str())->remove_accelerator(accel_group, entry.key, entry.modifiers);
    }
    accelerators.clear();

    set_accelerator_to_binding("fullscreen_item", "GTK_fullscreen");
    set_accelerator_to_binding("reset_item", "SoftReset");
    set_accelerator_to_binding("save_state_0", "QuickSave000");
    set_accelerator_to_binding("save_state_1", "QuickSave001");
    set_accelerator_to_binding("save_state_2", "QuickSave002");
    set_accelerator_to_binding("save_state_3", "QuickSave003");
    set_accelerator_to_binding("save_state_4", "QuickSave004");
    set_accelerator_to_binding("save_state_5", "QuickSave005");
    set_accelerator_to_binding("save_state_6", "QuickSave006");
    set_accelerator_to_binding("save_state_7", "QuickSave007");
    set_accelerator_to_binding("save_state_8", "QuickSave008");
    set_accelerator_to_binding("save_state_9", "QuickSave009");
    set_accelerator_to_binding("load_state_0", "QuickLoad000");
    set_accelerator_to_binding("load_state_1", "QuickLoad001");
    set_accelerator_to_binding("load_state_2", "QuickLoad002");
    set_accelerator_to_binding("load_state_3", "QuickLoad003");
    set_accelerator_to_binding("load_state_4", "QuickLoad004");
    set_accelerator_to_binding("load_state_5", "QuickLoad005");
    set_accelerator_to_binding("load_state_6", "QuickLoad006");
    set_accelerator_to_binding("load_state_7", "QuickLoad007");
    set_accelerator_to_binding("load_state_8", "QuickLoad008");
    set_accelerator_to_binding("load_state_9", "QuickLoad009");
    set_accelerator_to_binding("pause_item", "GTK_pause");
    set_accelerator_to_binding("save_spc_item", "GTK_save_spc");
    set_accelerator_to_binding("open_rom_item", "GTK_open_rom");
    set_accelerator_to_binding("record_movie_item", "BeginRecordingMovie");
    set_accelerator_to_binding("open_movie_item", "LoadMovie");
    set_accelerator_to_binding("stop_recording_item", "EndRecordingMovie");
    set_accelerator_to_binding("jump_to_frame_item", "GTK_seek_to_frame");
    set_accelerator_to_binding("reset_item", "SoftReset");
    set_accelerator_to_binding("hard_reset_item", "Reset");
    set_accelerator_to_binding("exit_item", "GTK_quit");

    // Special UI assignment
    set_accelerator_to_binding("hide_ui", "Escape Key");
}

void Snes9xWindow::resize_to_multiple(int factor)
{
    int h = (config->overscan ? 239 : 224) * factor;
    int w = h * S9xGetAspect() + 0.5;

    resize_viewport(w, h);
}

cairo_t *Snes9xWindow::get_cairo()
{
    if (cr)
        return cr;

    auto allocation = drawing_area->get_allocation();

    Cairo::RectangleInt rect = { 0, 0, allocation.get_width(), allocation.get_height() };
    gdk_drawing_context = drawing_area->get_window()->begin_draw_frame(Cairo::Region::create(rect));
    cr = gdk_drawing_context->get_cairo_context()->cobj();

    cairo_owned = true;
    return cr;
}

void Snes9xWindow::release_cairo()
{
    if (cairo_owned)
    {
        drawing_area->get_window()->end_draw_frame(gdk_drawing_context);
        gdk_drawing_context.clear();
        cairo_owned = false;
        cr = nullptr;
    }
}
