Files
HackerOoT/tools/Flips/flips-gtk.cpp
2024-02-04 13:04:13 +01:00

1092 lines
35 KiB
C++

//Module name: Floating IPS, GTK+ frontend
//Author: Alcaro
//Date: See Git history
//Licence: GPL v3.0 or higher
//List of assumptions made whose correctness is not guaranteed by GTK+:
//The character '9' is as wide as the widest of '0' '1' '2' '3' '4' '5' '6' '7' '8' '9'.
// Failure leads to: The BPS delta creation progress window being a little too small.
// Fixable: Not hard, but unlikely to be worth it.
//g_spawn_async does not write to argv or its pointed-to strings.
// Failure leads to: Corrupting the configuration.
// Fixable: Not hard, but unlikely to be worth it.
#include "flips.h"
#ifdef FLIPS_GTK
#define GDK_VERSION_MIN_REQUIRED GDK_VERSION_3_10
//I'd prefer enabling this, but that makes the GTK headers throw about 500 warnings about GtkFileChooserNative. probably missing ifdef
//#define GDK_VERSION_MAX_ALLOWED GDK_VERSION_3_10
#include <gtk/gtk.h>
class file_gtk : public file {
size_t size;
GFileInputStream* io;
public:
static file* create(const char * filename)
{
GFile* file = g_file_new_for_commandline_arg(filename);
GFileInputStream* io=g_file_read(file, NULL, NULL);
g_object_unref(file);
if (!io) return NULL;
return new file_gtk(io);
}
private:
file_gtk(GFileInputStream* io) : io(io)
{
GFileInfo* info = g_file_input_stream_query_info(io, G_FILE_ATTRIBUTE_STANDARD_SIZE, NULL, NULL);
size = g_file_info_get_size(info);
g_object_unref(info);
}
public:
size_t len() { return size; }
bool read(uint8_t* target, size_t start, size_t len)
{
g_seekable_seek(G_SEEKABLE(io), start, G_SEEK_SET, NULL, NULL);
gsize actualsize;
return (g_input_stream_read_all(G_INPUT_STREAM(io), target, len, &actualsize, NULL, NULL) && actualsize == len);
}
~file_gtk() { g_object_unref(io); }
};
file* file::create(const char * filename) { return file_gtk::create(filename); }
bool file::exists(const char * filename)
{
GFile* file = g_file_new_for_commandline_arg(filename);
bool ret = g_file_query_exists(file, NULL);
g_object_unref(file);
return ret;
}
class filewrite_gtk : public filewrite {
GOutputStream* io;
public:
static filewrite* create(const char * filename)
{
GFile* file = g_file_new_for_commandline_arg(filename);
if (!file) return NULL;
GFileOutputStream* io = g_file_replace(file, NULL, false, G_FILE_CREATE_NONE, NULL, NULL);
g_object_unref(file);
if (!io) return NULL;
return new filewrite_gtk(G_OUTPUT_STREAM(io));
}
private:
filewrite_gtk(GOutputStream* io) : io(io) {}
public:
bool append(const uint8_t* data, size_t len)
{
return g_output_stream_write_all(io, data, len, NULL, NULL, NULL);
}
~filewrite_gtk() { g_object_unref(io); }
};
filewrite* filewrite::create(const char * filename)
{
filewrite* ret = filewrite_gtk::create(filename); // g_file_create tries to create <dir>/.goutputstream-asdfghjkl
if (!ret) ret = filewrite::create_libc(filename); // if it fails (for example /dev/.goutputstream-foobar), try this instead
return ret;
}
class filemap_gtk : public filemap {
public:
GMappedFile* mapfile;
static filemap* create(const char * filename)
{
GMappedFile* mapfile = g_mapped_file_new(filename, false, NULL);
if (!mapfile) return NULL;
return new filemap_gtk(mapfile);
}
filemap_gtk(GMappedFile* mapfile) : mapfile(mapfile) {}
size_t len() { return g_mapped_file_get_length(mapfile); }
const uint8_t * ptr() { return (uint8_t*)g_mapped_file_get_contents(mapfile); }
~filemap_gtk() { g_mapped_file_unref(mapfile); }
};
filemap* filemap::create(const char * filename)
{
filemap* ret = filemap_gtk::create(filename);
if (!ret) ret = filemap::create_fallback(filename);
return ret;
}
static bool canShowGUI;
static GtkWidget* window;
static bool isFlatpak;
//struct {
// char signature[9];
// unsigned int lastPatchType;
// bool createFromAllFiles;
// bool openInEmulatorOnAssoc;
// bool autoSelectRom;
// gchar * emulator;
//} static state;
//#define cfgversion 5
static GtkWidget* windowBpsd;
static GtkWidget* labelBpsd;
static bool bpsdCancel;
//static void bpsdeltaCancel(GtkWindow* widget, gpointer user_data)
//{
// bpsdCancel=true;
//}
void bpsdeltaBegin()
{
bpsdCancel=false;
windowBpsd=gtk_window_new(GTK_WINDOW_TOPLEVEL);
if (window)
{
gtk_window_set_modal(GTK_WINDOW(windowBpsd), true);
gtk_window_set_transient_for(GTK_WINDOW(windowBpsd), GTK_WINDOW(window));
}
gtk_window_set_title(GTK_WINDOW(windowBpsd), flipsversion);
labelBpsd=gtk_label_new("Please wait... 99.9%");
gtk_container_add(GTK_CONTAINER(windowBpsd), labelBpsd);
GtkRequisition size;
gtk_widget_get_preferred_size(labelBpsd, NULL, &size);
gtk_label_set_text(GTK_LABEL(labelBpsd), "Please wait... 0.0%");
gtk_widget_set_size_request(labelBpsd, size.width, size.height);
gtk_window_set_resizable(GTK_WINDOW(windowBpsd), false);
gtk_misc_set_alignment(GTK_MISC(labelBpsd), 0.0f, 0.5f);
gtk_widget_show_all(windowBpsd);
}
bool bpsdeltaProgress(void* userdata, size_t done, size_t total)
{
if (bpsdeltaGetProgress(done, total))
{
gtk_label_set_text(GTK_LABEL(labelBpsd), bpsdProgStr);
}
gtk_main_iteration_do(false);
return !bpsdCancel;
}
void bpsdeltaEnd()
{
if (!bpsdCancel) gtk_widget_destroy(windowBpsd);
}
//'force' sets the filename even if the file doesn't exist
static void setoutpath(GtkFileChooser* dialog, const char * name, bool force)
{
if (!name) return;
gtk_file_chooser_set_uri(dialog, name);
if (!force) return;
gchar* filename = g_filename_from_uri(name, NULL, NULL);
if (filename)
{
gchar* basename = g_path_get_basename(filename);
gtk_file_chooser_set_current_name(dialog, basename ? basename : filename);
g_free(filename);
g_free(basename);
}
}
static char * SelectRom(const char * defaultname, const char * title, bool isForSaving)
{
GtkWidget* dialog;
if (!isForSaving)
{
dialog = gtk_file_chooser_dialog_new(title, GTK_WINDOW(window), GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL);
setoutpath(GTK_FILE_CHOOSER(dialog), defaultname, false);
}
else
{
dialog = gtk_file_chooser_dialog_new(title, GTK_WINDOW(window), GTK_FILE_CHOOSER_ACTION_SAVE,
"_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL);
setoutpath(GTK_FILE_CHOOSER(dialog), defaultname, true);
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), true);
}
GtkFileFilter* filterRom = gtk_file_filter_new();
gtk_file_filter_set_name(filterRom, "Most Common ROM Files");
gtk_file_filter_add_pattern(filterRom, "*.smc");
gtk_file_filter_add_pattern(filterRom, "*.sfc");
gtk_file_filter_add_pattern(filterRom, "*.nes");
gtk_file_filter_add_pattern(filterRom, "*.gb");
gtk_file_filter_add_pattern(filterRom, "*.gbc");
gtk_file_filter_add_pattern(filterRom, "*.gba");
gtk_file_filter_add_pattern(filterRom, "*.nds");
gtk_file_filter_add_pattern(filterRom, "*.vb");
gtk_file_filter_add_pattern(filterRom, "*.sms");
gtk_file_filter_add_pattern(filterRom, "*.smd");
gtk_file_filter_add_pattern(filterRom, "*.md");
gtk_file_filter_add_pattern(filterRom, "*.ngp");
gtk_file_filter_add_pattern(filterRom, "*.n64");
gtk_file_filter_add_pattern(filterRom, "*.z64");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filterRom);
GtkFileFilter* filterAll = gtk_file_filter_new();
gtk_file_filter_set_name(filterAll, "All files");
gtk_file_filter_add_pattern(filterAll, "*");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filterAll);
if (cfg.getint("allfiles")) gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), filterAll);
else gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), filterRom);
char * ret=NULL;
if (gtk_dialog_run(GTK_DIALOG(dialog))==GTK_RESPONSE_ACCEPT)
{
ret=gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(dialog));
}
GtkFileFilter* thisfilter=gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(dialog));
cfg.setint("allfiles", (thisfilter==filterAll));
gtk_widget_destroy(dialog);
return ret;
}
//returns path if demandLocal, else URI
static GSList * SelectPatches(bool allowMulti, bool demandLocal)
{
GtkWidget* dialog=gtk_file_chooser_dialog_new(allowMulti?"Select Patches to Use":"Select Patch to Use", GTK_WINDOW(window), GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL);
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), allowMulti);
gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(dialog), demandLocal);
GtkFileFilter* filter;
filter=gtk_file_filter_new();
gtk_file_filter_set_name(filter, "All supported patches (*.bps, *.ips)");
gtk_file_filter_add_pattern(filter, "*.bps");
gtk_file_filter_add_pattern(filter, "*.ips");
gtk_file_filter_add_pattern(filter, "*.ups");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
//apparently the file chooser takes ownership of the filter. would be nice to document that in gtk_file_chooser_set_filter...
filter=gtk_file_filter_new();
gtk_file_filter_set_name(filter, "All files");
gtk_file_filter_add_pattern(filter, "*");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
if (gtk_dialog_run(GTK_DIALOG(dialog))!=GTK_RESPONSE_ACCEPT)
{
gtk_widget_destroy(dialog);
return NULL;
}
GSList * ret;
if (demandLocal) ret=gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
else ret=gtk_file_chooser_get_uris(GTK_FILE_CHOOSER(dialog));
gtk_widget_destroy(dialog);
return ret;
}
static void ShowMessage(struct errorinfo errinf)
{
GtkMessageType errorlevels[]={ GTK_MESSAGE_OTHER, GTK_MESSAGE_OTHER, GTK_MESSAGE_WARNING, GTK_MESSAGE_WARNING, GTK_MESSAGE_ERROR, GTK_MESSAGE_ERROR };
GtkWidget* dialog=gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, errorlevels[errinf.level], GTK_BUTTONS_CLOSE, "%s",errinf.description);
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
}
enum worsterrorauto { ea_none, ea_warning, ea_invalid, ea_io_rom_write, ea_io_rom_read, ea_no_auto, ea_io_read_patch };
struct multiapplystateauto {
enum worsterrorauto error;
bool anySuccess;
const char * foundRom;
bool canUseFoundRom;
bool usingFoundRom;
};
static void ApplyPatchMultiAutoSub(gpointer data, gpointer user_data)
{
#define max(a,b) ((a)>(b)?(a):(b))
#define error(which) do { state->error=max(state->error, which); } while(0)
gchar * patchpath=(gchar*)data;
struct multiapplystateauto * state=(struct multiapplystateauto*)user_data;
file* patch = file::create(patchpath);
if (!patch)
{
state->canUseFoundRom=false;
error(ea_io_read_patch);
return;
}
bool possible;
const char * rompath=FindRomForPatch(patch, &possible);
if (state->usingFoundRom)
{
if (!rompath) rompath=state->foundRom;
else goto cleanup;
}
else
{
if (!rompath)
{
if (possible) state->canUseFoundRom=false;
error(ea_no_auto);
goto cleanup;
}
}
if (!state->foundRom) state->foundRom=rompath;
if (state->foundRom!=rompath) state->canUseFoundRom=false;
{
const char * romext=GetExtension(rompath);
gchar * outrompath=g_strndup(patchpath, strlen(patchpath)+strlen(romext)+1);
strcpy(GetExtension(outrompath), romext);
struct errorinfo errinf=ApplyPatchMem(patch, rompath, true, outrompath, NULL, true);
if (errinf.level==el_broken) error(ea_invalid);
if (errinf.level==el_notthis) error(ea_no_auto);
if (errinf.level==el_warning) error(ea_warning);
if (errinf.level<el_notthis) state->anySuccess=true;
else state->canUseFoundRom=false;
g_free(outrompath);
}
cleanup:
delete patch;
#undef max
#undef error
}
static bool ApplyPatchMultiAuto(GSList * filenames)
{
struct multiapplystateauto state;
state.error=ea_none;
state.anySuccess=false;
state.foundRom=NULL;
state.canUseFoundRom=true;
state.usingFoundRom=false;
g_slist_foreach(filenames, ApplyPatchMultiAutoSub, &state);
if (state.error==ea_no_auto && state.foundRom && state.canUseFoundRom)
{
state.usingFoundRom=true;
state.error=ea_none;
g_slist_foreach(filenames, ApplyPatchMultiAutoSub, &state);
}
if (state.anySuccess)
{
struct errorinfo messages[8]={
{ el_ok, "The patches were applied successfully!" },//ea_none
{ el_warning, "The patches were applied, but one or more may be mangled or improperly created..." },//ea_warning
{ el_warning, "Some patches were applied, but not all of the given patches are valid..." },//ea_invalid
{ el_warning, "Some patches were applied, but not all of the desired ROMs could be created..." },//ea_rom_io_write
{ el_warning, "Some patches were applied, but not all of the input ROMs could be read..." },//ea_io_rom_read
{ el_warning, "Some patches were applied, but not all of the required input ROMs could be located..." },//ea_no_auto
{ el_warning, "Some patches were applied, but not all of the given patches could be read..." },//ea_io_read_patch
{ el_broken, NULL },//ea_no_found
};
ShowMessage(messages[state.error]);
return true;
}
return false;
}
enum worsterror { e_none, e_warning_notthis, e_warning, e_invalid_this, e_invalid, e_io_write, e_io_read, e_io_read_rom };
struct multiapplystate {
const gchar * romext;
struct mem rommem;
bool anySuccess;
bool removeHeaders;
enum worsterror worsterror;
};
static void ApplyPatchMulti(gpointer data, gpointer user_data)
{
char * patchname=(char*)data;
struct multiapplystate * state=(struct multiapplystate*)user_data;
#define max(a,b) ((a)>(b)?(a):(b))
#define error(which) do { state->worsterror=max(state->worsterror, which); } while(0)
file* patch = file::create(patchname);
if (patch)
{
char * outromname=g_strndup(patchname, strlen(patchname)+strlen(state->romext)+1);
char * outromext=GetExtension(outromname);
strcpy(outromext, state->romext);
struct errorinfo errinf=ApplyPatchMem2(patch, state->rommem, state->removeHeaders, true, outromname, NULL);
if (errinf.level==el_broken) error(e_invalid);
if (errinf.level==el_notthis) error(e_invalid_this);
if (errinf.level==el_warning) error(e_warning);
if (errinf.level==el_unlikelythis) error(e_warning_notthis);
if (errinf.level<el_notthis) state->anySuccess=true;
delete patch;
g_free(outromname);
}
else error(e_io_read);
g_free(data);
#undef max
#undef error
}
static void a_ApplyPatch(GtkButton* widget, gpointer user_data)
{
gchar * filename=(gchar*)user_data;
GSList * filenames=NULL;
if (!filename)
{
filenames=SelectPatches(true, false);
if (!filenames) return;
if (!filenames->next) filename=(gchar*)filenames->data;
}
if (filename)//do not change to else, this is set if the user picks only one file
{
struct errorinfo errinf;
file* patchfile = file::create(filename);
if (!patchfile)
{
errinf=(struct errorinfo){ el_broken, "Couldn't read input patch. What exactly are you doing?" };
ShowMessage(errinf);
return;
}
char * inromname=NULL;
if (cfg.getint("autorom")) inromname=g_strdup(FindRomForPatch(patchfile, NULL)); // g_strdup(NULL) is NULL
if (!inromname) inromname=SelectRom(filename, "Select File to Patch", false);
if (!inromname) goto cleanup;
{
const char * inromext=GetExtension(inromname);
if (!inromext) inromext="";
char * outromname_d=g_strndup(filename, strlen(filename)+strlen(inromext)+1);
char * ext=GetExtension(outromname_d);
strcpy(ext, inromext);
char * outromname=SelectRom(outromname_d, "Select Output File", true);
if (outromname)
{
struct errorinfo errinf=ApplyPatchMem(patchfile, inromname, true, outromname, NULL, cfg.getint("autorom"));
ShowMessage(errinf);
}
g_free(inromname);
g_free(outromname_d);
g_free(outromname);
}
cleanup:
delete patchfile;
}
else
{
if (cfg.getint("autorom"))
{
if (ApplyPatchMultiAuto(filenames))
{
g_slist_free_full(filenames, g_free);
return;
}
}
struct multiapplystate state;
//picking one at random isn't always correct, but it's better than nothing
char * inromname=SelectRom((gchar*)filenames->data, "Select Base File", false);
if (!inromname) return;
state.romext=GetExtension(inromname);
if (!*state.romext) state.romext=".sfc";
filemap* map=filemap::create(inromname);
state.rommem=map->get();
state.removeHeaders=shouldRemoveHeader(inromname, state.rommem.len);
state.worsterror=e_none;
state.anySuccess=false;
g_slist_foreach(filenames, ApplyPatchMulti, &state);
g_free(inromname);
delete map;
struct errorinfo errormessages[2][8]={
{
//no error-free
{ el_ok, NULL },//e_none
{ el_warning, NULL},//e_warning_notthis
{ el_warning, NULL},//e_warning
{ el_broken, "None of these are valid patches for this ROM!" },//e_invalid_this
{ el_broken, "None of these are valid patches!" },//e_invalid
{ el_broken, "Couldn't write any ROMs. Are you on a read-only medium?" },//e_io_write
{ el_broken, "Couldn't read any patches. What exactly are you doing?" },//e_io_read
{ el_broken, "Couldn't read the input ROM. What exactly are you doing?" },//e_io_read_rom
},{
//at least one error-free
{ el_ok, "The patches were applied successfully!" },//e_none
{ el_warning, "The patches were applied, but one or more is unlikely to be intended for this ROM..." },//e_warning_notthis
{ el_warning, "The patches were applied, but one or more may be mangled or improperly created..." },//e_warning
{ el_warning, "Some patches were applied, but not all of the given patches are valid for this ROM..." },//e_invalid_this
{ el_warning, "Some patches were applied, but not all of the given patches are valid..." },//e_invalid
{ el_warning, "Some patches were applied, but not all of the desired ROMs could be created..." },//e_io_write
{ el_warning, "Some patches were applied, but not all of the given patches could be read..." },//e_io_read
{ el_broken, NULL,//e_io_read_rom
},
}};
ShowMessage(errormessages[state.anySuccess][state.worsterror]);
}
g_slist_free(filenames);
}
static void a_CreatePatch(GtkButton* widget, gpointer user_data)
{
char * inrom=NULL;
char * outrom=NULL;
char * patchname=NULL;
inrom=SelectRom(NULL, "Select ORIGINAL UNMODIFIED File to Use", false);
if (!inrom) goto cleanup;
outrom=SelectRom(inrom, "Select NEW MODIFIED File to Use", false);
if (!outrom) goto cleanup;
if (!strcmp(inrom, outrom))
{
ShowMessage((struct errorinfo){ el_broken, "That's the same file! You should really use two different files." });
goto cleanup;
}
struct {
const char * filter;
const char * description;
} static const typeinfo[]={
{ "*.bps", "BPS Patch File" },
{ "*.ips", "IPS Patch File" },
};
static const int numtypeinfo = sizeof(typeinfo)/sizeof(*typeinfo);
int lasttype;
lasttype = cfg.getint("lasttype", ty_bps);
{
char * defpatchname=g_strndup(outrom, strlen(outrom)+4+1);
char * ext=GetExtension(defpatchname);
strcpy(ext, typeinfo[lasttype-1].filter+1);
GtkWidget* dialog=gtk_file_chooser_dialog_new("Select File to Save As", GTK_WINDOW(window), GTK_FILE_CHOOSER_ACTION_SAVE,
"_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL);
setoutpath(GTK_FILE_CHOOSER(dialog), defpatchname, true);
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), true);
GtkFileFilter* filters[numtypeinfo];
for (size_t i=0;i<numtypeinfo;i++)
{
GtkFileFilter* filter=gtk_file_filter_new();
filters[i]=filter;
gtk_file_filter_set_name(filter, typeinfo[i].description);
gtk_file_filter_add_pattern(filter, typeinfo[i].filter);
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
}
gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), filters[lasttype-1]);
struct wrap {
GtkFileChooser* chooser;
GtkFileFilter** filters;
int current_filter_id;
void notify(GObject* self, GParamSpec* pspec)
{
GtkFileFilter* filter = gtk_file_chooser_get_filter(chooser);
int new_filter_id = 0;
while (filter != filters[new_filter_id])
new_filter_id++;
const char * prev_ext = typeinfo[current_filter_id].filter+1;
const char * new_ext = typeinfo[new_filter_id].filter+1;
char * current_name = gtk_file_chooser_get_current_name(chooser);
char * current_name_ext = strrchr(current_name, '.');
if (!strcmp(current_name_ext, prev_ext))
{
// good thing they're all the same length
strcpy(current_name_ext, new_ext);
gtk_file_chooser_set_current_name(chooser, current_name);
}
g_free(current_name);
current_filter_id = new_filter_id;
}
static void notify_s(GObject* self, GParamSpec* pspec, void* userdata)
{
((wrap*)userdata)->notify(self, pspec);
}
};
wrap filter_updater = { GTK_FILE_CHOOSER(dialog), filters, lasttype-1 };
g_signal_connect(dialog, "notify::filter", G_CALLBACK(&wrap::notify_s), &filter_updater);
if (gtk_dialog_run(GTK_DIALOG(dialog))==GTK_RESPONSE_ACCEPT)
{
patchname=gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(dialog));
}
GtkFileFilter* filter=gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(dialog));
for (int i=0;i<numtypeinfo;i++)
{
if (filter==filters[i])
{
cfg.setint("lasttype", i+1);
lasttype = i+1;
}
}
gtk_widget_destroy(dialog);
}
if (!patchname) goto cleanup;
bpsdCancel=false;
struct errorinfo errinf;
errinf=CreatePatch(inrom, outrom, (patchtype)lasttype, NULL, patchname);
if (!bpsdCancel) ShowMessage(errinf);
cleanup:
g_free(inrom);
g_free(outrom);
g_free(patchname);
}
static void a_SetEmulatorFor(GtkButton* widget, gpointer user_data);
static void a_ApplyRun(GtkButton* widget, gpointer user_data)
{
gchar * patchname=(gchar*)user_data;
if (!patchname)
{
GSList * patchnames=SelectPatches(false, true);
if (!patchnames) return;
patchname=(gchar*)patchnames->data;
g_slist_free(patchnames);
}
file* patchfile = file::create(patchname);
gchar * romname=NULL;
{
if (!patchfile)
{
ShowMessage((struct errorinfo){ el_broken, "Couldn't read input patch. What exactly are you doing?" });
goto cleanup;
}
if (cfg.getint("autorom")) romname=g_strdup(FindRomForPatch(patchfile, NULL)); // g_strdup(NULL) is NULL
if (!romname)
{
char* patch_uri = g_filename_to_uri(patchname, NULL, NULL);
romname=SelectRom(patch_uri, "Select Base File", false);
g_free(patch_uri);
}
if (!romname) goto cleanup;
if (!GetEmuFor(romname)) a_SetEmulatorFor(NULL, romname);
if (!GetEmuFor(romname)) goto cleanup;
//gchar * outromname;
//gint fd=g_file_open_tmp("flipsXXXXXX.smc", &outromname, NULL);
gchar * outromname_rel=g_strndup(patchname, strlen(patchname)+4+1);
strcpy(GetExtension(outromname_rel), GetExtension(romname));
GFile* outrom_file=g_file_new_for_commandline_arg(outromname_rel);
g_free(outromname_rel);
gchar * outromname;
if (g_file_is_native(outrom_file)) outromname=g_file_get_path(outrom_file);
else outromname=g_file_get_uri(outrom_file);
g_object_unref(outrom_file);
struct errorinfo errinf=ApplyPatchMem(patchfile, romname, true, outromname, NULL, cfg.getint("autorom"));
if (errinf.level!=el_ok) ShowMessage(errinf);
if (errinf.level>=el_notthis) goto cleanup;
gchar * patchend=GetBaseName(patchname);
*patchend='\0';
const gchar * argv[3];
argv[0]=GetEmuFor(romname);
argv[1]=outromname;
argv[2]=NULL;
GPid pid;
GError* error=NULL;
if (!g_spawn_async(*patchname ? patchname : NULL, (gchar**)argv, NULL, G_SPAWN_DEFAULT, NULL, NULL, &pid, &error))
{
//g_unlink(tempname);//apparently this one isn't in the headers.
ShowMessage((struct errorinfo){ el_broken, error->message });
g_error_free(error);
}
else g_spawn_close_pid(pid);
g_free(outromname);
//close(fd);
}
cleanup:
delete patchfile;
g_free(patchname);
g_free(romname);
}
static void flatpakDisable(GtkWidget* widget)
{
if (isFlatpak)
{
gtk_widget_set_sensitive(widget, false);
gtk_widget_set_tooltip_text(widget, "Running in emulators is not available in Flatpak");
}
}
static void a_SetEmulator(GtkButton* widget, gpointer user_data);
static void a_ShowSettings(GtkButton* widget, gpointer user_data)
{
//used mnemonics:
//E - Select Emulator
//M - Create ROM
//U - Run in Emulator
//A - Enable automatic ROM selector
GtkWidget* settingswindow=gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(settingswindow), flipsversion);
gtk_window_set_resizable(GTK_WINDOW(settingswindow), false);
gtk_window_set_modal(GTK_WINDOW(settingswindow), true);
gtk_window_set_transient_for(GTK_WINDOW(settingswindow), GTK_WINDOW(window));
g_signal_connect(settingswindow, "destroy", G_CALLBACK(gtk_main_quit), NULL);
GtkGrid* grid=GTK_GRID(gtk_grid_new());
gtk_grid_set_row_spacing(grid, 3);
GtkWidget* button=gtk_button_new_with_mnemonic("Select _Emulator");
g_signal_connect(button, "clicked", G_CALLBACK(a_SetEmulator), settingswindow);
gtk_grid_attach(grid, button, 0,0, 1,1);
flatpakDisable(button);
GtkWidget* text=gtk_label_new("When opening through associations:");
gtk_grid_attach(grid, text, 0,1, 1,1);
GtkGrid* radioGrid=GTK_GRID(gtk_grid_new());
gtk_grid_set_column_homogeneous(radioGrid, true);
GtkWidget* emuAssoc;
emuAssoc=gtk_radio_button_new_with_mnemonic(NULL, "Create RO_M");
gtk_grid_attach(radioGrid, emuAssoc, 0,0, 1,1);
emuAssoc=gtk_radio_button_new_with_mnemonic_from_widget(GTK_RADIO_BUTTON(emuAssoc), "R_un in Emulator");
gtk_grid_attach(radioGrid, emuAssoc, 1,0, 1,1);
g_object_ref(emuAssoc);//otherwise it, and its value, gets eaten when I close the window, before I can save its value anywhere
flatpakDisable(emuAssoc);
if (cfg.getint("assocemu")) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(emuAssoc), true);
gtk_grid_attach(grid, GTK_WIDGET(radioGrid), 0,2, 1,1);
GtkWidget* autoRom;
autoRom=gtk_check_button_new_with_mnemonic("Enable _automatic ROM selector");
if (cfg.getint("autorom")) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(autoRom), true);
g_object_ref(autoRom);
gtk_grid_attach(grid, autoRom, 0,3, 1,1);
flatpakDisable(autoRom);
gtk_container_add(GTK_CONTAINER(settingswindow), GTK_WIDGET(grid));
gtk_widget_show_all(settingswindow);
gtk_main();
cfg.setint("assocemu", (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(emuAssoc))));
cfg.setint("autorom", (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(autoRom))));
g_object_unref(emuAssoc);
g_object_unref(autoRom);
}
static gboolean filterExecOnly(const GtkFileFilterInfo* filter_info, gpointer data)
{
GFile* file=g_file_new_for_uri(filter_info->uri);
GFileInfo* info=g_file_query_info(file, G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE, G_FILE_QUERY_INFO_NONE, NULL, NULL);
bool ret=g_file_info_get_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE);
g_object_unref(file);
g_object_unref(info);
return ret;
}
static void a_SetEmulatorFor(GtkButton* widget, gpointer user_data)
{
GtkWidget* dialog=gtk_file_chooser_dialog_new("Select Emulator to Use", GTK_WINDOW(window), GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL);
gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(dialog), true);
GtkFileFilter* filter=gtk_file_filter_new();
gtk_file_filter_set_name(filter, "Executable files");
gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_URI, filterExecOnly, NULL, NULL);
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
const char * emu_path = GetEmuFor((const char*)user_data);
char* emu_uri = emu_path ? g_filename_to_uri(emu_path, NULL, NULL) : NULL;
setoutpath(GTK_FILE_CHOOSER(dialog), emu_uri, false);
g_free(emu_uri);
if (gtk_dialog_run(GTK_DIALOG(dialog))==GTK_RESPONSE_ACCEPT)
{
SetEmuFor((const char*)user_data, gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)));
}
gtk_widget_destroy(dialog);
}
static void SetEmuActivate(GtkTreeView* tree_view, GtkTreePath* path, GtkTreeViewColumn* column, gpointer user_data)
{
GtkListStore* list = GTK_LIST_STORE(gtk_tree_view_get_model(tree_view));
GtkTreeModel* model = gtk_tree_view_get_model(tree_view);
GtkTreeIter iter;
gtk_tree_model_get_iter(model, &iter, path);
char* ext;
gtk_tree_model_get(model, &iter, 0,&ext, -1);
char* name = g_strdup_printf(".%s", ext);
a_SetEmulatorFor(NULL, name);
gtk_list_store_set(list, &iter, 1,GetEmuFor(name), -1);
g_free(name);
g_free(ext);
}
static void SetEmuDelete(GtkButton* widget, gpointer user_data)
{
GtkTreeView* listview = GTK_TREE_VIEW(user_data);
GList* list = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(listview), NULL);
if (!list) return;
GtkTreeModel* model = gtk_tree_view_get_model(listview);
GtkTreeIter it;
gtk_tree_model_get_iter(model, &it, (GtkTreePath*)list->data);
char* ext;
gtk_tree_model_get(model, &it, 0,&ext, -1);
char* name = g_strdup_printf("emu.%s", ext);
cfg.set(name, NULL);
g_free(name);
g_free(ext);
gtk_list_store_remove(GTK_LIST_STORE(model), &it);
g_list_free_full(list, (GDestroyNotify)gtk_tree_path_free);
}
static void a_SetEmulator(GtkButton* widget, gpointer user_data)
{
GtkWidget* emuwindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(emuwindow), flipsversion);
gtk_window_set_modal(GTK_WINDOW(emuwindow), true);
gtk_window_set_transient_for(GTK_WINDOW(emuwindow), GTK_WINDOW(user_data));
gtk_window_set_default_size (GTK_WINDOW(emuwindow), 300, 200);
g_signal_connect(emuwindow, "destroy", G_CALLBACK(gtk_main_quit), NULL);
GtkGrid* grid = GTK_GRID(gtk_grid_new());
gtk_grid_set_row_spacing(grid, 3);
GtkListStore* list = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
for (size_t i=0;i<cfg.getcount();i++)
{
const char * name = cfg.getnamebyid(i);
const char * value = cfg.getvaluebyid(i);
if (strncmp(name, "emu.", strlen("emu.")) != 0) continue;
if (value==NULL) continue;
GtkTreeIter iter;
gtk_list_store_append(list, &iter);
gtk_list_store_set(list, &iter, 0,name+strlen("emu."), 1,value, -1);
//gtk_list_store_set(list, &iter, 0,name, 1,value, -1);
}
const char * columns[]={"Type", "Emulator"};
//I just remembered why I hate this specific widget. It's so complex.
GtkTreeView* listview = GTK_TREE_VIEW(gtk_tree_view_new());
GtkCellRenderer* render=gtk_cell_renderer_text_new();
for (int i=0;i<2;i++)
{
gtk_tree_view_insert_column_with_attributes(listview, -1, columns[i], render, "text", i, NULL);
GtkTreeViewColumn* col=gtk_tree_view_get_column(listview, i);
int width = gtk_tree_view_column_get_width(col);
gtk_tree_view_column_set_fixed_width(col, width*2);
if (i==1) gtk_tree_view_column_set_expand(col, true);
}
gtk_tree_view_set_model(listview, GTK_TREE_MODEL(list));
g_signal_connect(listview, "row-activated", G_CALLBACK(SetEmuActivate), NULL);
GtkWidget* scroll = gtk_scrolled_window_new(NULL, NULL);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_container_add(GTK_CONTAINER(scroll), GTK_WIDGET(listview));
gtk_widget_set_hexpand(scroll, true);
gtk_widget_set_vexpand(scroll, true);
gtk_grid_attach(grid, scroll, 0,0, 1,1);
GtkWidget* btnDelete = gtk_button_new_with_label("Delete");
g_signal_connect(btnDelete, "clicked", G_CALLBACK(SetEmuDelete), listview);
gtk_grid_attach(grid, btnDelete, 0,1, 1,1);
gtk_container_add(GTK_CONTAINER(emuwindow), GTK_WIDGET(grid));
gtk_widget_show_all(emuwindow);
gtk_main();
//TODO: pull values back from the ListStore
//discard everything in the config, then add new values from the store
//those stores are a bit easier to work with if I don't insist they interoperate with non-GTK things
}
//gchar * get_cfgpath()
//{
// static gchar * cfgpath=NULL;
// if (!cfgpath) cfgpath=g_build_filename(g_get_user_config_dir(), "flipscfg", NULL);
// return cfgpath;
//}
//
//void GUILoadConfig()
//{
// if (!canShowGUI) return;
//
// state.lastPatchType = atoi(cfg.get(TEXT("lasttype")));
// state.createFromAllFiles = atoi(cfg.get(TEXT("allfiles")));
// state.openInEmulatorOnAssoc = atoi(cfg.get(TEXT("assocemu")));
// state.autoSelectRom = atoi(cfg.get(TEXT("autorom")));
//
// //const char *
// //state.autoSelectRom = atoi(cfg.get(TEXT("autorom")));
//
// struct mem cfgin = file::read(get_cfgpath());
// if (cfgin.len>=10+1+1+1+1+4+4 && !memcmp(cfgin.ptr, "FlipscfgG", 9) && cfgin.ptr[9]==cfgversion)
// {
// state.lastPatchType=cfgin.ptr[10];
// state.createFromAllFiles=cfgin.ptr[11];
// state.openInEmulatorOnAssoc=cfgin.ptr[12];
// state.autoSelectRom=cfgin.ptr[13];
// int len=0;
// len|=cfgin.ptr[14]<<24;
// len|=cfgin.ptr[15]<<16;
// len|=cfgin.ptr[16]<<8;
// len|=cfgin.ptr[17]<<0;
// if (len==0) state.emulator=NULL;
// else
// {
// state.emulator=(gchar*)g_malloc(len+1);
// memcpy(state.emulator, cfgin.ptr+22, len);
// state.emulator[len]=0;
// }
// struct mem romlist={cfgin.ptr+22+len, 0};
// romlist.len|=cfgin.ptr[18]<<24;
// romlist.len|=cfgin.ptr[19]<<16;
// romlist.len|=cfgin.ptr[20]<<8;
// romlist.len|=cfgin.ptr[21]<<0;
// SetRomList(romlist);
// }
// else
// {
// memset(&state, 0, sizeof(state));
// state.lastPatchType=ty_bps;
// }
// free(cfgin.ptr);
//}
int GUIShow(const char * filename)
{
if (!canShowGUI)
{
g_warning("couldn't parse command line arguments, what are you doing?");
usage();
}
//copied a few lines from libgtk, I don't want to call gtk_init if I'm not going to poke the GUI
GdkDisplay* display=gdk_display_open(gdk_get_display_arg_name());
if (!display) display=gdk_display_get_default();
if (!display)
{
g_warning("couldn't connect to display, fix it or use command line");
usage();
}
gdk_display_manager_set_default_display(gdk_display_manager_get(), display);
if (filename)
{
window=NULL;
if (cfg.getint("assocemu")==false) a_ApplyPatch(NULL, g_strdup(filename));
else a_ApplyRun(NULL, g_strdup(filename));
return 0;
}
window=gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window), flipsversion);
gtk_window_set_resizable(GTK_WINDOW(window), false);
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
GtkGrid* grid=GTK_GRID(gtk_grid_new());
gtk_grid_set_row_homogeneous(grid, true);
gtk_grid_set_column_homogeneous(grid, true);
gtk_grid_set_row_spacing(grid, 5);
gtk_grid_set_column_spacing(grid, 5);
GtkWidget* button;
#define button(x, y, text, function) \
button=gtk_button_new_with_mnemonic(text); \
g_signal_connect(button, "clicked", function, NULL); \
gtk_grid_attach(grid, button, x, y, 1, 1);
button(0,0, "_Apply Patch", G_CALLBACK(a_ApplyPatch));
button(1,0, "_Create Patch", G_CALLBACK(a_CreatePatch));
button(0,1, "Apply and _Run", G_CALLBACK(a_ApplyRun));
flatpakDisable(button);
button(1,1, "_Settings", G_CALLBACK(a_ShowSettings));
#undef button
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(grid));
gtk_widget_show_all(window);
gtk_main();
//int emulen=state.emulator?strlen(state.emulator):0;
//struct mem romlist=GetRomList();
//struct mem cfgout=(struct mem){ NULL, 10+1+1+1+1+4+4+emulen+romlist.len };
//cfgout.ptr=(uint8_t*)g_malloc(cfgout.len);
//memcpy(cfgout.ptr, "FlipscfgG", 9);
//cfgout.ptr[9]=cfgversion;
//cfgout.ptr[10]=state.lastPatchType;
//cfgout.ptr[11]=state.createFromAllFiles;
//cfgout.ptr[12]=state.openInEmulatorOnAssoc;
//cfgout.ptr[13]=state.autoSelectRom;
//cfgout.ptr[14]=emulen>>24;
//cfgout.ptr[15]=emulen>>16;
//cfgout.ptr[16]=emulen>>8;
//cfgout.ptr[17]=emulen>>0;
//cfgout.ptr[18]=romlist.len>>24;
//cfgout.ptr[19]=romlist.len>>16;
//cfgout.ptr[20]=romlist.len>>8;
//cfgout.ptr[21]=romlist.len>>0;
//memcpy(cfgout.ptr+22, state.emulator, emulen);
//memcpy(cfgout.ptr+22+emulen, romlist.ptr, romlist.len);
//filewrite::write(get_cfgpath(), cfgout);
return 0;
}
int main(int argc, char * argv[])
{
g_set_prgname("com.github.Alcaro.Flips");
canShowGUI = gtk_parse_args(&argc, &argv);
isFlatpak = (access("/.flatpak-info", F_OK) == 0);
cfg.init_file(g_build_filename(g_get_user_config_dir(), "flipscfg", NULL));
return flipsmain(argc, argv);
}
#endif