From efdd938dc358dc718431e03b3b08c4b92959c4b6 Mon Sep 17 00:00:00 2001 From: En-En <39373446+En-En-Code@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:32:03 +0000 Subject: [PATCH] GTK*: Action Replay cheat menu (+ other cheat improvements) (#847) * GTK cheats UI inputs/displays hex for address (offset) * Added range bound to keep internal cheat data within specified size The decision to change the value column to store G_TYPE_UINT is purely pragmatic--having all data be treated and displayed as unsized is simpler than writing a custom display to handle signedness and cleaner than 1-3 byte ints appearing unsigned and 4 byte int appearing signed. * Added index and cheat type data to ListStore My implementation plan is to use a GtkTreeModelFilter to create separate section for internal and ActionReplay cheats, but I was not convinced the indices in the tree path would correspond to the indices in , hence the additional column. * Filter raw and AR cheats, display both filters in UI, patch up raw update/delete * Action Replay UI elements [GTK] * Memory leak fixes + some additional clean-up comments and small bug patches * Backport to GTK2 --- desmume/src/cheatSystem.cpp | 12 +- desmume/src/cheatSystem.h | 5 +- desmume/src/frontend/posix/gtk/Makefile.am | 1 + desmume/src/frontend/posix/gtk/cheatsGTK.cpp | 301 +++++++++++---- desmume/src/frontend/posix/gtk/meson.build | 1 + desmume/src/frontend/posix/gtk/utilsGTK.cpp | 326 ++++++++++++++++ desmume/src/frontend/posix/gtk/utilsGTK.h | 65 ++++ desmume/src/frontend/posix/gtk2/Makefile.am | 1 + desmume/src/frontend/posix/gtk2/cheatsGTK.cpp | 301 +++++++++++---- desmume/src/frontend/posix/gtk2/meson.build | 1 + desmume/src/frontend/posix/gtk2/utilsGTK.cpp | 355 ++++++++++++++++++ desmume/src/frontend/posix/gtk2/utilsGTK.h | 92 +++++ 12 files changed, 1319 insertions(+), 142 deletions(-) create mode 100644 desmume/src/frontend/posix/gtk/utilsGTK.cpp create mode 100644 desmume/src/frontend/posix/gtk/utilsGTK.h create mode 100644 desmume/src/frontend/posix/gtk2/utilsGTK.cpp create mode 100644 desmume/src/frontend/posix/gtk2/utilsGTK.h diff --git a/desmume/src/cheatSystem.cpp b/desmume/src/cheatSystem.cpp index 03993b54d..4c85ae952 100755 --- a/desmume/src/cheatSystem.cpp +++ b/desmume/src/cheatSystem.cpp @@ -1,5 +1,5 @@ /* - Copyright (C) 2009-2023 DeSmuME team + Copyright (C) 2009-2024 DeSmuME team This file is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -849,6 +849,16 @@ bool CHEATS::remove(const size_t pos) return didRemoveItem; } +void CHEATS::toggle(bool enabled, const size_t pos) +{ + this->_list[pos].enabled = (enabled) ? 1 : 0; +} + +void CHEATS::toggle(u8 enabled, const size_t pos) +{ + this->toggle((enabled != 0), pos); +} + void CHEATS::getListReset() { this->_currentGet = 0; diff --git a/desmume/src/cheatSystem.h b/desmume/src/cheatSystem.h index e4bda9f4c..3f7b6ebd9 100755 --- a/desmume/src/cheatSystem.h +++ b/desmume/src/cheatSystem.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2009-2023 DeSmuME team + Copyright (C) 2009-2024 DeSmuME team This file is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -120,6 +120,9 @@ public: bool remove(const size_t pos); + void toggle(bool enabled, const size_t pos); + void toggle(u8 enablbed, const size_t pos); + void getListReset(); bool getList(CHEATS_LIST *cheat); CHEATS_LIST* getListPtr(); diff --git a/desmume/src/frontend/posix/gtk/Makefile.am b/desmume/src/frontend/posix/gtk/Makefile.am index 63a1d7b08..cbef8a0e2 100644 --- a/desmume/src/frontend/posix/gtk/Makefile.am +++ b/desmume/src/frontend/posix/gtk/Makefile.am @@ -21,6 +21,7 @@ desmume_SOURCES = \ desmume.h desmume.cpp \ dTool.h dToolsList.cpp \ tools/ioregsView.cpp tools/ioregsView.h \ + utilsGTK.h utilsGTK.cpp \ cheatsGTK.h cheatsGTK.cpp \ main.cpp main.h diff --git a/desmume/src/frontend/posix/gtk/cheatsGTK.cpp b/desmume/src/frontend/posix/gtk/cheatsGTK.cpp index ca74ba403..04cb94d0c 100644 --- a/desmume/src/frontend/posix/gtk/cheatsGTK.cpp +++ b/desmume/src/frontend/posix/gtk/cheatsGTK.cpp @@ -1,6 +1,6 @@ -/* cheats.cpp - this file is part of DeSmuME +/* cheatsGTK.cpp - this file is part of DeSmuME * - * Copyright (C) 2006-2023 DeSmuME Team + * Copyright (C) 2006-2024 DeSmuME Team * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,12 +18,12 @@ * Boston, MA 02111-1307, USA. */ - #include #include #include #include "cheatsGTK.h" #include "cheatSystem.h" +#include "utilsGTK.h" #include "main.h" #include "desmume.h" @@ -31,10 +31,13 @@ #define GPOINTER_TO_INT(p) ((gint) (glong) (p)) enum { + COLUMN_INDEX, + COLUMN_TYPE, COLUMN_ENABLED, COLUMN_SIZE, COLUMN_HI, COLUMN_LO, + COLUMN_AR, COLUMN_DESC, NUM_COL }; @@ -56,10 +59,13 @@ static struct { gint type; gint column; } columnTable[]={ + { "Index", TYPE_STRING, COLUMN_INDEX}, + { "Type", TYPE_STRING, COLUMN_TYPE}, { "Enabled", TYPE_TOGGLE, COLUMN_ENABLED}, { "Size", TYPE_COMBO, COLUMN_SIZE}, - { "Offset", TYPE_STRING, COLUMN_HI}, + { "Address", TYPE_STRING, COLUMN_HI}, { "Value", TYPE_STRING, COLUMN_LO}, + { "AR Code", TYPE_STRING, COLUMN_AR}, { "Description", TYPE_STRING, COLUMN_DESC} }; @@ -67,7 +73,7 @@ static GtkWidget *win = NULL; static BOOL shouldBeRunning = FALSE; // --------------------------------------------------------------------------------- -// SEARCH +// CHEATS MENU // --------------------------------------------------------------------------------- static void @@ -75,28 +81,28 @@ enabled_toggled(GtkCellRendererToggle * cell, gchar * path_str, gpointer data) { GtkTreeModel *model = (GtkTreeModel *) data; - GtkTreeIter iter; + GtkTreeIter iter, f_iter; GtkTreePath *path = gtk_tree_path_new_from_string(path_str); gboolean guiEnabled; - gtk_tree_model_get_iter(model, &iter, path); - gtk_tree_model_get(model, &iter, COLUMN_ENABLED, &guiEnabled, -1); + gtk_tree_model_get_iter(model, &f_iter, path); + gtk_tree_model_filter_convert_iter_to_child_iter( + GTK_TREE_MODEL_FILTER(model), &iter, &f_iter); + GtkTreeModel *store = + gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model)); + + gtk_tree_model_get(store, &iter, COLUMN_ENABLED, &guiEnabled, -1); guiEnabled ^= 1; const bool cheatEnabled = (guiEnabled) ? true : false; CHEATS_LIST tempCheatItem; u32 ii; - GtkTreePath *path1; - path1 = gtk_tree_model_get_path (model, &iter); - ii = gtk_tree_path_get_indices (path)[0]; + gtk_tree_model_get(store, &iter, COLUMN_INDEX, &ii, -1); - cheats->copyItemFromIndex(ii, tempCheatItem); + cheats->toggle(cheatEnabled, ii); - cheats->update(tempCheatItem.size, tempCheatItem.code[0][0], tempCheatItem.code[0][1], tempCheatItem.description, - cheatEnabled, ii); - - gtk_list_store_set(GTK_LIST_STORE(model), &iter, COLUMN_ENABLED, guiEnabled, -1); + gtk_list_store_set(GTK_LIST_STORE(store), &iter, COLUMN_ENABLED, guiEnabled, -1); gtk_tree_path_free(path); } @@ -107,51 +113,74 @@ static void cheat_list_modify_cheat(GtkCellRendererText * cell, { GtkTreeModel *model = (GtkTreeModel *) data; GtkTreePath *path = gtk_tree_path_new_from_string(path_string); - GtkTreeIter iter; + GtkTreeIter iter, f_iter; gint column = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(cell), "column")); - gtk_tree_model_get_iter(model, &iter, path); + gtk_tree_model_get_iter(model, &f_iter, path); + gtk_tree_path_free(path); + + gtk_tree_model_filter_convert_iter_to_child_iter( + GTK_TREE_MODEL_FILTER(model), &iter, &f_iter); + GtkTreeModel *store = + gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model)); { u32 ii; - GtkTreePath *path1; CHEATS_LIST cheat; - path1 = gtk_tree_model_get_path (model, &iter); - ii = gtk_tree_path_get_indices (path)[0]; + gtk_tree_model_get(store, &iter, COLUMN_INDEX, &ii, -1); - cheats->copyItemFromIndex(ii, cheat); - - gtk_tree_path_free (path1); + cheats->copyItemFromIndex(ii, cheat); if (column == COLUMN_LO || column == COLUMN_HI || column == COLUMN_SIZE) { - u32 v = atoi(new_text); + u32 v = 0; + u32 data; switch (column) { case COLUMN_SIZE: - cheats->update(v-1, cheat.code[0][0], cheat.code[0][1], + v = atoi(new_text); + // If the size is reduced, the data is currently contains may be + // out of range, so cap it at its maximum value. + data = std::min(0xFFFFFFFF >> (24 - ((v - 1) << 3)), + cheat.code[0][1]); + cheats->update(v-1, cheat.code[0][0], data, cheat.description, cheat.enabled, ii); + gtk_list_store_set(GTK_LIST_STORE(store), &iter, COLUMN_LO, + data, -1); break; case COLUMN_HI: + sscanf(new_text, "%x", &v); + v &= 0x0FFFFFFF; cheats->update(cheat.size, v, cheat.code[0][1], cheat.description, cheat.enabled, ii); break; case COLUMN_LO: + v = atoi(new_text); + v = std::min(0xFFFFFFFF >> (24 - (cheat.size << 3)), v); cheats->update(cheat.size, cheat.code[0][0], v, cheat.description, cheat.enabled, ii); break; } - gtk_list_store_set(GTK_LIST_STORE(model), &iter, column, - atoi(new_text), -1); + gtk_list_store_set(GTK_LIST_STORE(store), &iter, column, v, -1); } else if (column == COLUMN_DESC){ - cheats->update(cheat.size, cheat.code[0][0], cheat.code[0][1], - g_strdup(new_text), cheat.enabled, ii); - gtk_list_store_set(GTK_LIST_STORE(model), &iter, column, - g_strdup(new_text), -1); + cheats->setDescription(new_text, ii); + gtk_list_store_set(GTK_LIST_STORE(store), &iter, column, + new_text, -1); + } else if (column == COLUMN_AR) { + // Safety: CHEATS::update_AR, though it takes `code` as not const, + // only performs a non-null check and passes `code` to + // CHEATS::XXCodeFromString as const, therefore new_text (should) + // never be modified + bool isValid = + cheats->update_AR(const_cast(new_text), + cheat.description, cheat.enabled, ii); + if (isValid) { + gtk_list_store_set(GTK_LIST_STORE(store), &iter, column, + new_text, -1); + } } - } } @@ -160,17 +189,29 @@ static void cheat_list_remove_cheat(GtkWidget * widget, gpointer data) GtkTreeView *tree = (GtkTreeView *) data; GtkTreeSelection *selection = gtk_tree_view_get_selection (tree); GtkTreeModel *model = gtk_tree_view_get_model (tree); - GtkTreeIter iter; + GtkTreeIter iter, f_iter; - if (gtk_tree_selection_get_selected (selection, NULL, &iter)){ + if (gtk_tree_selection_get_selected (selection, NULL, &f_iter)){ u32 ii; + gboolean valid; GtkTreePath *path; - path = gtk_tree_model_get_path (model, &iter); - ii = gtk_tree_path_get_indices (path)[0]; + path = gtk_tree_model_get_path (model, &f_iter); + gtk_tree_model_filter_convert_iter_to_child_iter( + GTK_TREE_MODEL_FILTER(model), &iter, &f_iter); + GtkTreeModel *store = + gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model)); - gtk_list_store_remove(GTK_LIST_STORE(model), &iter); + gtk_tree_model_get(store, &iter, COLUMN_INDEX, &ii, -1); + + valid = gtk_list_store_remove(GTK_LIST_STORE(store), &iter); cheats->remove(ii); + while (valid) { + gtk_list_store_set(GTK_LIST_STORE(store), &iter, COLUMN_INDEX, ii, + -1); + ii++; + valid = gtk_tree_model_iter_next(store, &iter); + } gtk_tree_path_free (path); } @@ -181,17 +222,42 @@ static void cheat_list_add_cheat(GtkWidget * widget, gpointer data) #define NEW_DESC "New cheat" GtkListStore *store = (GtkListStore *) data; GtkTreeIter iter; - cheats->add(1, 0, 0, g_strdup(NEW_DESC), false); + // Safety: CHEATS::add only uses `description` to call CHEATS::update, which + // only uses it to call CHEATS::setDescription, which takes `description` as + // const. + cheats->add(0, 0, 0, const_cast(NEW_DESC), false); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, + COLUMN_INDEX, cheats->getListSize() - 1, + COLUMN_TYPE, 0, COLUMN_ENABLED, FALSE, COLUMN_SIZE, 1, COLUMN_HI, 0, COLUMN_LO, 0, COLUMN_DESC, NEW_DESC, -1); - #undef NEW_DESC } +static void cheat_list_add_cheat_AR(GtkWidget *widget, gpointer data) +{ +#define NEW_DESC "New cheat" +#define NEW_AR "00000000 00000000" + GtkListStore *store = (GtkListStore *) data; + GtkTreeIter iter; + // Safety: CHEATS::add_AR only uses `code` to call , `description` to call + // CHEATS::setDescription, which takes the variable as const. + cheats->add_AR(const_cast(NEW_AR), const_cast(NEW_DESC), + false); + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, + COLUMN_INDEX, cheats->getListSize() - 1, + COLUMN_TYPE, 1, + COLUMN_ENABLED, FALSE, + COLUMN_AR, NEW_AR, + COLUMN_DESC, NEW_DESC, -1); +#undef NEW_DESC +#undef NEW_AR +} + static GtkTreeModel * create_numbers_model (void) { #define N_NUMBERS 4 @@ -222,9 +288,21 @@ static GtkTreeModel * create_numbers_model (void) #undef N_NUMBERS } -static void cheat_list_add_columns(GtkTreeView * tree, GtkListStore * store) +static void cheat_list_address_to_hex(GtkTreeViewColumn *column, + GtkCellRenderer *renderer, + GtkTreeModel *model, GtkTreeIter *iter, + gpointer data) { + gint addr; + gtk_tree_model_get(model, iter, COLUMN_HI, &addr, -1); + gchar *hex_addr = g_strdup_printf("0x0%07X", addr); + g_object_set(renderer, "text", hex_addr, NULL); + g_free(hex_addr); +} +static void cheat_list_add_columns(GtkTreeView * tree, GtkListStore * store, + u8 cheat_type) +{ GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(tree)); static GtkTreeModel * size_model; @@ -240,10 +318,13 @@ static void cheat_list_add_columns(GtkTreeView * tree, GtkListStore * store) attrib = "active"; break; case TYPE_STRING: - renderer = gtk_cell_renderer_text_new(); + if (cheat_type == CHEAT_TYPE_INTERNAL) + renderer = gtk_cell_renderer_text_new(); + else if (cheat_type == CHEAT_TYPE_AR) + renderer = desmume_cell_renderer_ndtext_new(); g_object_set(renderer, "editable", TRUE, NULL); g_signal_connect(renderer, "edited", - G_CALLBACK(cheat_list_modify_cheat), store); + G_CALLBACK(cheat_list_modify_cheat), model); attrib = "text"; break; case TYPE_COMBO: @@ -257,20 +338,29 @@ static void cheat_list_add_columns(GtkTreeView * tree, GtkListStore * store) NULL); g_object_unref(size_model); g_signal_connect(renderer, "edited", - G_CALLBACK(cheat_list_modify_cheat), store); + G_CALLBACK(cheat_list_modify_cheat), model); attrib = "text"; break; } + gint c = columnTable[ii].column; column = gtk_tree_view_column_new_with_attributes(columnTable[ii]. caption, renderer, - attrib, columnTable[ii].column, + attrib, c, NULL); - g_object_set_data(G_OBJECT(renderer), "column", - GINT_TO_POINTER(columnTable[ii].column)); - gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + if (c == COLUMN_HI && cheat_type == CHEAT_TYPE_INTERNAL) { + gtk_tree_view_column_set_cell_data_func( + column, renderer, cheat_list_address_to_hex, NULL, NULL); + } + if (c == COLUMN_ENABLED || c == COLUMN_DESC || + ((c == COLUMN_SIZE || c == COLUMN_HI || c == COLUMN_LO) && + cheat_type == CHEAT_TYPE_INTERNAL) || + (c == COLUMN_AR && cheat_type == CHEAT_TYPE_AR)) { + g_object_set_data(G_OBJECT(renderer), "column", + GINT_TO_POINTER(columnTable[ii].column)); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + } } - } static void cheatListEnd() @@ -282,8 +372,11 @@ static void cheatListEnd() static GtkListStore *cheat_list_populate() { - GtkListStore *store = gtk_list_store_new (5, G_TYPE_BOOLEAN, - G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING); + // COLUMN_INDEX, COLUMN_TYPE, COLUMN_ENABLED, COLUMN_SIZE, + // COLUMN_HI, COLUMN_LO, COLUMN_AR, COLUMN_DESC + GtkListStore *store = gtk_list_store_new ( + 8, G_TYPE_INT, G_TYPE_INT, G_TYPE_BOOLEAN, G_TYPE_INT, + G_TYPE_INT, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING); CHEATS_LIST cheat; u32 chsize = cheats->getListSize(); @@ -291,45 +384,113 @@ static GtkListStore *cheat_list_populate() GtkTreeIter iter; cheats->copyItemFromIndex(ii, cheat); gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, - COLUMN_ENABLED, cheat.enabled, - COLUMN_SIZE, cheat.size+1, - COLUMN_HI, cheat.code[0][0], - COLUMN_LO, cheat.code[0][1], - COLUMN_DESC, cheat.description, - -1); + if (cheat.type == CHEAT_TYPE_INTERNAL) { + gtk_list_store_set(store, &iter, COLUMN_INDEX, ii, + COLUMN_TYPE, cheat.type, + COLUMN_ENABLED, cheat.enabled, + COLUMN_SIZE, cheat.size + 1, + COLUMN_HI, cheat.code[0][0], + COLUMN_LO, cheat.code[0][1], + COLUMN_DESC, cheat.description, -1); + } else if (cheat.type == CHEAT_TYPE_AR) { + u32 cheat_len = cheat.num; + char *cheat_str = (char *) malloc(18 * cheat_len); + cheat_str[0] = '\0'; + + // Safety: "%08X" is 8 bytes (x2), " " and "\n" are 1 each for 18 + // bytes each strdup_printf called cheat_len times for the size of + // the malloc. g_strlcat emulates BSD's strlcat, so on the last + // iteration, a NUL-terminator is writted instead of the last + // trailing newline. + for (u32 jj = 0; jj < cheat_len; jj++) { + gchar *tmp = g_strdup_printf("%08X %08X\n", cheat.code[jj][0], + cheat.code[jj][1]); + g_strlcat(cheat_str, tmp, 18 * cheat_len); + g_free(tmp); + } + + gtk_list_store_set(store, &iter, COLUMN_INDEX, ii, + COLUMN_TYPE, cheat.type, + COLUMN_ENABLED, cheat.enabled, + COLUMN_AR, cheat_str, + COLUMN_DESC, cheat.description, -1); + free(cheat_str); + } } return store; } -static GtkWidget *cheat_list_create_ui() +static gboolean cheat_list_is_raw(GtkTreeModel *model, GtkTreeIter *iter, + gpointer data) +{ + gint type = CHEAT_TYPE_EMPTY; + gtk_tree_model_get(model, iter, COLUMN_TYPE, &type, -1); + return type == CHEAT_TYPE_INTERNAL; +} + +static gboolean cheat_list_is_ar(GtkTreeModel *model, GtkTreeIter *iter, + gpointer data) +{ + gint type = CHEAT_TYPE_EMPTY; + gtk_tree_model_get(model, iter, COLUMN_TYPE, &type, -1); + return type == CHEAT_TYPE_AR; +} + +static void cheat_list_create_ui() { GtkListStore *store = cheat_list_populate(); - GtkWidget *tree = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store)); + GtkTreeModel *filter_raw = + gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL); + gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filter_raw), + cheat_list_is_raw, NULL, NULL); + GtkWidget *tree_raw = gtk_tree_view_new_with_model(filter_raw); + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1); GtkWidget *hbbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL); GtkWidget *button; - - gtk_container_add(GTK_CONTAINER(box), GTK_WIDGET(tree)); + + gtk_container_add(GTK_CONTAINER(box), GTK_WIDGET(tree_raw)); gtk_container_add(GTK_CONTAINER(box), GTK_WIDGET(hbbox)); gtk_container_add(GTK_CONTAINER(win), GTK_WIDGET(box)); - button = gtk_button_new_with_label("add cheat"); + button = gtk_button_new_with_label("Add internal cheat"); g_signal_connect (button, "clicked", G_CALLBACK (cheat_list_add_cheat), store); gtk_container_add(GTK_CONTAINER(hbbox),button); - button = gtk_button_new_with_label("Remove cheat"); - g_signal_connect (button, "clicked", G_CALLBACK (cheat_list_remove_cheat), tree); + button = gtk_button_new_with_label("Remove internal cheat"); + g_signal_connect (button, "clicked", G_CALLBACK (cheat_list_remove_cheat), + GTK_TREE_VIEW(tree_raw)); gtk_container_add(GTK_CONTAINER(hbbox),button); - cheat_list_add_columns(GTK_TREE_VIEW(tree), store); - + GtkTreeModel *filter_ar = + gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL); + gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filter_ar), + cheat_list_is_ar, NULL, NULL); + GtkWidget *tree_ar = gtk_tree_view_new_with_model(filter_ar); + + hbbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL); + gtk_container_add(GTK_CONTAINER(box), GTK_WIDGET(tree_ar)); + gtk_container_add(GTK_CONTAINER(box), GTK_WIDGET(hbbox)); + + button = gtk_button_new_with_label("Add Action Replay cheat"); + gtk_container_add(GTK_CONTAINER(hbbox), button); + g_signal_connect(button, "clicked", G_CALLBACK(cheat_list_add_cheat_AR), + store); + + button = gtk_button_new_with_label("Remove Action Replay cheat"); + g_signal_connect(button, "clicked", G_CALLBACK(cheat_list_remove_cheat), + GTK_TREE_VIEW(tree_ar)); + gtk_container_add(GTK_CONTAINER(hbbox), button); + + cheat_list_add_columns(GTK_TREE_VIEW(tree_raw), store, CHEAT_TYPE_INTERNAL); + cheat_list_add_columns(GTK_TREE_VIEW(tree_ar), store, CHEAT_TYPE_AR); + /* Setup the selection handler */ GtkTreeSelection *select; - select = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree)); + select = gtk_tree_view_get_selection (GTK_TREE_VIEW(tree_raw)); + gtk_tree_selection_set_mode (select, GTK_SELECTION_SINGLE); + select = gtk_tree_view_get_selection (GTK_TREE_VIEW(tree_ar)); gtk_tree_selection_set_mode (select, GTK_SELECTION_SINGLE); - - return tree; } void CheatList(GSimpleAction *action, GVariant *parameter, gpointer user_data) diff --git a/desmume/src/frontend/posix/gtk/meson.build b/desmume/src/frontend/posix/gtk/meson.build index 53f4b3883..a656958a2 100644 --- a/desmume/src/frontend/posix/gtk/meson.build +++ b/desmume/src/frontend/posix/gtk/meson.build @@ -19,6 +19,7 @@ desmume_src = [ 'desmume.cpp', 'dToolsList.cpp', 'tools/ioregsView.cpp', + 'utilsGTK.cpp', 'cheatsGTK.cpp', 'main.cpp', gresource, diff --git a/desmume/src/frontend/posix/gtk/utilsGTK.cpp b/desmume/src/frontend/posix/gtk/utilsGTK.cpp new file mode 100644 index 000000000..b25321bd6 --- /dev/null +++ b/desmume/src/frontend/posix/gtk/utilsGTK.cpp @@ -0,0 +1,326 @@ +/* utilsGTK.cpp - this file is part of DeSmuME + * + * Copyright (C) 2024 DeSmuME Team + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "utilsGTK.h" +#include + +/* + A C++ implementation of a GtkCellRendererText subclass which handles + newline-delimited text and allows for editing that text with the ability + for users to add newlines, based off of a GPLv2+ Python implementation here: + https://gitlab.gnome.org/GNOME/gtk/-/issues/175#note_487323 +*/ + +/* + DESMUME_ENTRY_ND: + An object similar to an Entry, but which allows for newlines to be + inserted by holding Shift, Ctrl, or Alt along with pressing Enter. +*/ + +typedef struct +{ + GtkWidget *scroll; + GtkWidget *editor; + gboolean editing_canceled; +} DesmumeEntryNdPrivate; + +typedef enum +{ + PROP_EDITING_CANCELED = 1, + ENTRY_ND_NUM_PROP, +} DesmumeEntryNdProperty; + +static GParamSpec *entry_nd_properties[ENTRY_ND_NUM_PROP] = { + NULL, +}; + +// Declared here to statisfy the type creation macro, but defined further down. +static void desmume_entry_nd_editable_init(GtkEditableInterface *iface); +static void desmume_entry_nd_cell_editable_init(GtkCellEditableIface *iface); + +G_DEFINE_TYPE_WITH_CODE( + DesmumeEntryNd, desmume_entry_nd, GTK_TYPE_EVENT_BOX, + G_ADD_PRIVATE(DesmumeEntryNd) + G_IMPLEMENT_INTERFACE(GTK_TYPE_EDITABLE, desmume_entry_nd_editable_init) + G_IMPLEMENT_INTERFACE(GTK_TYPE_CELL_EDITABLE, + desmume_entry_nd_cell_editable_init)) + +static void desmume_entry_nd_set_property(GObject *object, guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + DesmumeEntryNd *entry_nd = DESMUME_ENTRY_ND(object); + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + switch ((DesmumeEntryNdProperty) property_id) { + case PROP_EDITING_CANCELED: + if (priv->editing_canceled != g_value_get_boolean(value)) { + priv->editing_canceled = g_value_get_boolean(value); + g_object_notify(object, "editing-canceled"); + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void desmume_entry_nd_get_property(GObject *object, guint property_id, + GValue *value, GParamSpec *pspec) +{ + DesmumeEntryNd *entry_nd = DESMUME_ENTRY_ND(object); + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + switch ((DesmumeEntryNdProperty) property_id) { + case PROP_EDITING_CANCELED: + g_value_set_boolean(value, priv->editing_canceled); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static gboolean desmume_entry_nd_key_press(GtkWidget *widget, + GdkEventKey *event, gpointer data) +{ + DesmumeEntryNd *entry_nd = (DesmumeEntryNd *) data; + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + // Allow the editor to decide how to handle the key event, except key events + // which its parent TextView needs to handle itself + gboolean doPropagate = GDK_EVENT_PROPAGATE; + guint kv = event->keyval; + guint mod = event->state; + + if ((kv == GDK_KEY_Return || kv == GDK_KEY_KP_Enter || + kv == GDK_KEY_ISO_Enter) && + !(mod & (GDK_CONTROL_MASK | GDK_SHIFT_MASK | GDK_MOD1_MASK))) { + // Enter + Ctrl, Shift, or Mod1 (commonly Alt), enter a newline in the + // editor, but otherwise act as confirm for the TextView + priv->editing_canceled = FALSE; + doPropagate = GDK_EVENT_STOP; + gtk_cell_editable_editing_done(GTK_CELL_EDITABLE(entry_nd)); + } else if (kv == GDK_KEY_Escape) { + priv->editing_canceled = TRUE; + doPropagate = GDK_EVENT_STOP; + gtk_cell_editable_editing_done(GTK_CELL_EDITABLE(entry_nd)); + } + return doPropagate; +} + +static gboolean desmume_entry_nd_button_press(GtkWidget *widget, + GdkEventButton *event, + gpointer data) +{ + GtkTextView *editor = (GtkTextView *) widget; + GtkWidgetClass *klass = GTK_WIDGET_GET_CLASS(editor); + klass->button_press_event(widget, event); + // We have explicitly described how to handle mouse button events, so do not + // propagate. + return GDK_EVENT_STOP; +} + +static void desmume_entry_nd_dispose(GObject *object) +{ + DesmumeEntryNd *entry_nd = DESMUME_ENTRY_ND(object); + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + // Recursively destroys contained objects, so destroys the editor as well + gtk_widget_destroy(priv->scroll); + + G_OBJECT_CLASS(desmume_entry_nd_parent_class)->dispose(object); +} + +static void desmume_entry_nd_finalize(GObject *object) +{ + G_OBJECT_CLASS(desmume_entry_nd_parent_class)->finalize(object); +} + +static void desmume_entry_nd_class_init(DesmumeEntryNdClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + object_class->set_property = desmume_entry_nd_set_property; + object_class->get_property = desmume_entry_nd_get_property; + + object_class->dispose = desmume_entry_nd_dispose; + object_class->finalize = desmume_entry_nd_finalize; + + entry_nd_properties[PROP_EDITING_CANCELED] = + g_param_spec_boolean("editing-canceled", "Editing Canceled", + "The edit was canceled", FALSE, G_PARAM_READWRITE); + g_object_class_install_properties(object_class, ENTRY_ND_NUM_PROP, + entry_nd_properties); +} + +static void desmume_entry_nd_init(DesmumeEntryNd *entry_nd) +{ + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + priv->scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(priv->scroll), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + + priv->editor = gtk_text_view_new(); + gtk_text_view_set_editable(GTK_TEXT_VIEW(priv->editor), TRUE); + gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(priv->editor), FALSE); + + g_signal_connect(priv->editor, "key-press-event", + G_CALLBACK(desmume_entry_nd_key_press), entry_nd); + g_signal_connect(priv->editor, "button-press-event", + G_CALLBACK(desmume_entry_nd_button_press), NULL); + + gtk_container_add(GTK_CONTAINER(priv->scroll), priv->editor); + gtk_container_add(GTK_CONTAINER(entry_nd), priv->scroll); +} + +static void desmume_entry_nd_start_editing(GtkCellEditable *cell_editable, + GdkEvent *event) +{ + DesmumeEntryNd *entry_nd = DESMUME_ENTRY_ND(cell_editable); + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + gtk_widget_show_all(GTK_WIDGET(entry_nd)); + gtk_widget_grab_focus(GTK_WIDGET(priv->editor)); + + // Highlight the entirety of the editor's text + GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->editor)); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buf, &start, &end); + gtk_text_buffer_select_range(buf, &start, &end); +} + +static gchar *desmume_entry_nd_get_text(DesmumeEntryNd *entry_nd) +{ + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->editor)); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buf, &start, &end); + + return gtk_text_buffer_get_text(buf, &start, &end, TRUE); +} + +static void desmume_entry_nd_set_text(DesmumeEntryNd *entry_nd, + const gchar *text) +{ + DesmumeEntryNdPrivate *priv; + priv = (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->editor)); + gtk_text_buffer_set_text(buf, text, strlen(text)); +} + +static void desmume_entry_nd_editable_init(GtkEditableInterface *iface) { } + +static void desmume_entry_nd_cell_editable_init(GtkCellEditableIface *iface) +{ + iface->start_editing = desmume_entry_nd_start_editing; +} + +GtkWidget *desmume_entry_nd_new() +{ + return (GtkWidget *) g_object_new(DESMUME_TYPE_ENTRY_ND, NULL); +} + +/* + DESMUME_CELL_RENDERER_ND_TEXT: + A subclass of GtkCellRendererText which creates our DesmumeEntryNd instead + of a GtkEntry, which allows a cell in a TreeView to accepts newlines. +*/ + +G_DEFINE_TYPE(DesmumeCellRendererNdtext, desmume_cell_renderer_ndtext, + GTK_TYPE_CELL_RENDERER_TEXT) + +static void desmume_cell_renderer_ndtext_editing_done(GtkCellEditable *entry_nd, + gpointer data) +{ + gboolean canceled; + g_object_get(entry_nd, "editing-canceled", &canceled, NULL); + if (!canceled) { + const gchar *path = + (gchar *) g_object_get_data(G_OBJECT(entry_nd), "full-text"); + gchar *new_text = desmume_entry_nd_get_text(DESMUME_ENTRY_ND(entry_nd)); + + guint signal_id = + g_signal_lookup("edited", DESMUME_TYPE_CELL_RENDERER_NDTEXT); + g_signal_emit(data, signal_id, 0, path, new_text); + g_free(new_text); + } + gtk_cell_editable_remove_widget(GTK_CELL_EDITABLE(entry_nd)); +} + +static GtkCellEditable *desmume_cell_renderer_ndtext_start_editing( + GtkCellRenderer *cell, GdkEvent *event, GtkWidget *widget, + const gchar *path, const GdkRectangle *background_area, + const GdkRectangle *cell_area, GtkCellRendererState flags) +{ + DesmumeCellRendererNdtext *ndtext = DESMUME_CELL_RENDERER_NDTEXT(cell); + gboolean editable; + g_object_get(G_OBJECT(ndtext), "editable", &editable, NULL); + if (!editable) + return NULL; + + gchar *text; + g_object_get(G_OBJECT(ndtext), "text", &text, NULL); + + GtkWidget *entry_nd = desmume_entry_nd_new(); + if (text != NULL) { + desmume_entry_nd_set_text(DESMUME_ENTRY_ND(entry_nd), text); + g_free(text); + } + g_object_set_data_full(G_OBJECT(entry_nd), "full-text", g_strdup(path), + g_free); + + g_signal_connect(entry_nd, "editing-done", + G_CALLBACK(desmume_cell_renderer_ndtext_editing_done), + ndtext); + return GTK_CELL_EDITABLE(entry_nd); +} + +static void +desmume_cell_renderer_ndtext_class_init(DesmumeCellRendererNdtextClass *klass) +{ + GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS(klass); + cell_class->start_editing = desmume_cell_renderer_ndtext_start_editing; +} + +static void desmume_cell_renderer_ndtext_init(DesmumeCellRendererNdtext *ndtext) +{ +} + +GtkCellRenderer *desmume_cell_renderer_ndtext_new() +{ + return GTK_CELL_RENDERER( + g_object_new(DESMUME_TYPE_CELL_RENDERER_NDTEXT, NULL)); +} diff --git a/desmume/src/frontend/posix/gtk/utilsGTK.h b/desmume/src/frontend/posix/gtk/utilsGTK.h new file mode 100644 index 000000000..a7ccd549b --- /dev/null +++ b/desmume/src/frontend/posix/gtk/utilsGTK.h @@ -0,0 +1,65 @@ +/* utilsGTK.h - this file is part of DeSmuME + * + * Copyright (C) 2024 DeSmuME Team + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __UTILS_GTK_H__ +#define __UTILS_GTK_H__ + +#include + +G_BEGIN_DECLS + +#define DESMUME_TYPE_ENTRY_ND (desmume_entry_nd_get_type()) +G_DECLARE_FINAL_TYPE(DesmumeEntryNd, desmume_entry_nd, DESMUME, ENTRY_ND, + GtkEventBox) + +struct _DesmumeEntryNd +{ + GtkEventBox parent; +}; + +struct _DesmumeEntryNdClass +{ + GtkEventBoxClass parent_class; +}; + +GType desmume_entry_nd_get_type(void) G_GNUC_CONST; +GtkEventBox *entry_nd_new(void); + +#define DESMUME_TYPE_CELL_RENDERER_NDTEXT \ + (desmume_cell_renderer_ndtext_get_type()) +G_DECLARE_FINAL_TYPE(DesmumeCellRendererNdtext, desmume_cell_renderer_ndtext, + DESMUME, CELL_RENDERER_NDTEXT, GtkCellRendererText) + +struct _DesmumeCellRendererNdtext +{ + GtkCellRendererText parent; +}; + +struct _DesmumeCellRendererNdtextClass +{ + GtkCellRendererTextClass parent_class; +}; + +GType desmume_cell_renderer_ndtext_get_type(void) G_GNUC_CONST; +GtkCellRenderer *desmume_cell_renderer_ndtext_new(void); + +G_END_DECLS + +#endif /*__UTILS_GTK_H__*/ diff --git a/desmume/src/frontend/posix/gtk2/Makefile.am b/desmume/src/frontend/posix/gtk2/Makefile.am index 7554f5b52..6dc462ab6 100644 --- a/desmume/src/frontend/posix/gtk2/Makefile.am +++ b/desmume/src/frontend/posix/gtk2/Makefile.am @@ -21,6 +21,7 @@ desmume_SOURCES = \ desmume.h desmume.cpp \ dTool.h dToolsList.cpp \ tools/ioregsView.cpp tools/ioregsView.h \ + utilsGTK.h utilsGTK.cpp \ cheatsGTK.h cheatsGTK.cpp \ main.cpp main.h diff --git a/desmume/src/frontend/posix/gtk2/cheatsGTK.cpp b/desmume/src/frontend/posix/gtk2/cheatsGTK.cpp index 411243f4e..8211b6038 100644 --- a/desmume/src/frontend/posix/gtk2/cheatsGTK.cpp +++ b/desmume/src/frontend/posix/gtk2/cheatsGTK.cpp @@ -1,6 +1,6 @@ -/* cheats.cpp - this file is part of DeSmuME +/* cheatsGTK.cpp - this file is part of DeSmuME * - * Copyright (C) 2006-2023 DeSmuME Team + * Copyright (C) 2006-2024 DeSmuME Team * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,12 +18,12 @@ * Boston, MA 02111-1307, USA. */ - #include #include #include #include "cheatsGTK.h" #include "cheatSystem.h" +#include "utilsGTK.h" #include "main.h" #include "desmume.h" @@ -31,10 +31,13 @@ #define GPOINTER_TO_INT(p) ((gint) (glong) (p)) enum { + COLUMN_INDEX, + COLUMN_TYPE, COLUMN_ENABLED, COLUMN_SIZE, COLUMN_HI, COLUMN_LO, + COLUMN_AR, COLUMN_DESC, NUM_COL }; @@ -56,10 +59,13 @@ static struct { gint type; gint column; } columnTable[]={ + { "Index", TYPE_STRING, COLUMN_INDEX}, + { "Type", TYPE_STRING, COLUMN_TYPE}, { "Enabled", TYPE_TOGGLE, COLUMN_ENABLED}, { "Size", TYPE_COMBO, COLUMN_SIZE}, - { "Offset", TYPE_STRING, COLUMN_HI}, + { "Address", TYPE_STRING, COLUMN_HI}, { "Value", TYPE_STRING, COLUMN_LO}, + { "AR Code", TYPE_STRING, COLUMN_AR}, { "Description", TYPE_STRING, COLUMN_DESC} }; @@ -67,7 +73,7 @@ static GtkWidget *win = NULL; static BOOL shouldBeRunning = FALSE; // --------------------------------------------------------------------------------- -// SEARCH +// CHEATS MENU // --------------------------------------------------------------------------------- static void @@ -75,28 +81,28 @@ enabled_toggled(GtkCellRendererToggle * cell, gchar * path_str, gpointer data) { GtkTreeModel *model = (GtkTreeModel *) data; - GtkTreeIter iter; + GtkTreeIter iter, f_iter; GtkTreePath *path = gtk_tree_path_new_from_string(path_str); gboolean guiEnabled; - gtk_tree_model_get_iter(model, &iter, path); - gtk_tree_model_get(model, &iter, COLUMN_ENABLED, &guiEnabled, -1); + gtk_tree_model_get_iter(model, &f_iter, path); + gtk_tree_model_filter_convert_iter_to_child_iter( + GTK_TREE_MODEL_FILTER(model), &iter, &f_iter); + GtkTreeModel *store = + gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model)); + + gtk_tree_model_get(store, &iter, COLUMN_ENABLED, &guiEnabled, -1); guiEnabled ^= 1; const bool cheatEnabled = (guiEnabled) ? true : false; CHEATS_LIST tempCheatItem; u32 ii; - GtkTreePath *path1; - path1 = gtk_tree_model_get_path (model, &iter); - ii = gtk_tree_path_get_indices (path)[0]; + gtk_tree_model_get(store, &iter, COLUMN_INDEX, &ii, -1); - cheats->copyItemFromIndex(ii, tempCheatItem); + cheats->toggle(cheatEnabled, ii); - cheats->update(tempCheatItem.size, tempCheatItem.code[0][0], tempCheatItem.code[0][1], tempCheatItem.description, - cheatEnabled, ii); - - gtk_list_store_set(GTK_LIST_STORE(model), &iter, COLUMN_ENABLED, guiEnabled, -1); + gtk_list_store_set(GTK_LIST_STORE(store), &iter, COLUMN_ENABLED, guiEnabled, -1); gtk_tree_path_free(path); } @@ -107,51 +113,74 @@ static void cheat_list_modify_cheat(GtkCellRendererText * cell, { GtkTreeModel *model = (GtkTreeModel *) data; GtkTreePath *path = gtk_tree_path_new_from_string(path_string); - GtkTreeIter iter; + GtkTreeIter iter, f_iter; gint column = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(cell), "column")); - gtk_tree_model_get_iter(model, &iter, path); + gtk_tree_model_get_iter(model, &f_iter, path); + gtk_tree_path_free(path); + + gtk_tree_model_filter_convert_iter_to_child_iter( + GTK_TREE_MODEL_FILTER(model), &iter, &f_iter); + GtkTreeModel *store = + gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model)); { u32 ii; - GtkTreePath *path1; CHEATS_LIST cheat; - path1 = gtk_tree_model_get_path (model, &iter); - ii = gtk_tree_path_get_indices (path)[0]; + gtk_tree_model_get(store, &iter, COLUMN_INDEX, &ii, -1); - cheats->copyItemFromIndex(ii, cheat); - - gtk_tree_path_free (path1); + cheats->copyItemFromIndex(ii, cheat); if (column == COLUMN_LO || column == COLUMN_HI || column == COLUMN_SIZE) { - u32 v = atoi(new_text); + u32 v = 0; + u32 data; switch (column) { case COLUMN_SIZE: - cheats->update(v-1, cheat.code[0][0], cheat.code[0][1], + v = atoi(new_text); + // If the size is reduced, the data is currently contains may be + // out of range, so cap it at its maximum value. + data = std::min(0xFFFFFFFF >> (24 - ((v - 1) << 3)), + cheat.code[0][1]); + cheats->update(v-1, cheat.code[0][0], data, cheat.description, cheat.enabled, ii); + gtk_list_store_set(GTK_LIST_STORE(store), &iter, COLUMN_LO, + data, -1); break; case COLUMN_HI: + sscanf(new_text, "%x", &v); + v &= 0x0FFFFFFF; cheats->update(cheat.size, v, cheat.code[0][1], cheat.description, cheat.enabled, ii); break; case COLUMN_LO: + v = atoi(new_text); + v = std::min(0xFFFFFFFF >> (24 - (cheat.size << 3)), v); cheats->update(cheat.size, cheat.code[0][0], v, cheat.description, cheat.enabled, ii); break; } - gtk_list_store_set(GTK_LIST_STORE(model), &iter, column, - atoi(new_text), -1); + gtk_list_store_set(GTK_LIST_STORE(store), &iter, column, v, -1); } else if (column == COLUMN_DESC){ - cheats->update(cheat.size, cheat.code[0][0], cheat.code[0][1], - g_strdup(new_text), cheat.enabled, ii); - gtk_list_store_set(GTK_LIST_STORE(model), &iter, column, - g_strdup(new_text), -1); + cheats->setDescription(new_text, ii); + gtk_list_store_set(GTK_LIST_STORE(store), &iter, column, + new_text, -1); + } else if (column == COLUMN_AR) { + // Safety: CHEATS::update_AR, though it takes `code` as not const, + // only performs a non-null check and passes `code` to + // CHEATS::XXCodeFromString as const, therefore new_text (should) + // never be modified + bool isValid = + cheats->update_AR(const_cast(new_text), + cheat.description, cheat.enabled, ii); + if (isValid) { + gtk_list_store_set(GTK_LIST_STORE(store), &iter, column, + new_text, -1); + } } - } } @@ -160,17 +189,29 @@ static void cheat_list_remove_cheat(GtkWidget * widget, gpointer data) GtkTreeView *tree = (GtkTreeView *) data; GtkTreeSelection *selection = gtk_tree_view_get_selection (tree); GtkTreeModel *model = gtk_tree_view_get_model (tree); - GtkTreeIter iter; + GtkTreeIter iter, f_iter; - if (gtk_tree_selection_get_selected (selection, NULL, &iter)){ + if (gtk_tree_selection_get_selected (selection, NULL, &f_iter)){ u32 ii; + gboolean valid; GtkTreePath *path; - path = gtk_tree_model_get_path (model, &iter); - ii = gtk_tree_path_get_indices (path)[0]; + path = gtk_tree_model_get_path (model, &f_iter); + gtk_tree_model_filter_convert_iter_to_child_iter( + GTK_TREE_MODEL_FILTER(model), &iter, &f_iter); + GtkTreeModel *store = + gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model)); - gtk_list_store_remove(GTK_LIST_STORE(model), &iter); + gtk_tree_model_get(store, &iter, COLUMN_INDEX, &ii, -1); + + valid = gtk_list_store_remove(GTK_LIST_STORE(store), &iter); cheats->remove(ii); + while (valid) { + gtk_list_store_set(GTK_LIST_STORE(store), &iter, COLUMN_INDEX, ii, + -1); + ii++; + valid = gtk_tree_model_iter_next(store, &iter); + } gtk_tree_path_free (path); } @@ -181,17 +222,42 @@ static void cheat_list_add_cheat(GtkWidget * widget, gpointer data) #define NEW_DESC "New cheat" GtkListStore *store = (GtkListStore *) data; GtkTreeIter iter; - cheats->add(1, 0, 0, g_strdup(NEW_DESC), false); + // Safety: CHEATS::add only uses `description` to call CHEATS::update, which + // only uses it to call CHEATS::setDescription, which takes `description` as + // const. + cheats->add(0, 0, 0, const_cast(NEW_DESC), false); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, + COLUMN_INDEX, cheats->getListSize() - 1, + COLUMN_TYPE, 0, COLUMN_ENABLED, FALSE, COLUMN_SIZE, 1, COLUMN_HI, 0, COLUMN_LO, 0, COLUMN_DESC, NEW_DESC, -1); - #undef NEW_DESC } +static void cheat_list_add_cheat_AR(GtkWidget *widget, gpointer data) +{ +#define NEW_DESC "New cheat" +#define NEW_AR "00000000 00000000" + GtkListStore *store = (GtkListStore *) data; + GtkTreeIter iter; + // Safety: CHEATS::add_AR only uses `code` to call , `description` to call + // CHEATS::setDescription, which takes the variable as const. + cheats->add_AR(const_cast(NEW_AR), const_cast(NEW_DESC), + false); + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, + COLUMN_INDEX, cheats->getListSize() - 1, + COLUMN_TYPE, 1, + COLUMN_ENABLED, FALSE, + COLUMN_AR, NEW_AR, + COLUMN_DESC, NEW_DESC, -1); +#undef NEW_DESC +#undef NEW_AR +} + static GtkTreeModel * create_numbers_model (void) { #define N_NUMBERS 4 @@ -222,9 +288,21 @@ static GtkTreeModel * create_numbers_model (void) #undef N_NUMBERS } -static void cheat_list_add_columns(GtkTreeView * tree, GtkListStore * store) +static void cheat_list_address_to_hex(GtkTreeViewColumn *column, + GtkCellRenderer *renderer, + GtkTreeModel *model, GtkTreeIter *iter, + gpointer data) { + gint addr; + gtk_tree_model_get(model, iter, COLUMN_HI, &addr, -1); + gchar *hex_addr = g_strdup_printf("0x0%07X", addr); + g_object_set(renderer, "text", hex_addr, NULL); + g_free(hex_addr); +} +static void cheat_list_add_columns(GtkTreeView * tree, GtkListStore * store, + u8 cheat_type) +{ GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(tree)); static GtkTreeModel * size_model; @@ -240,10 +318,13 @@ static void cheat_list_add_columns(GtkTreeView * tree, GtkListStore * store) attrib = "active"; break; case TYPE_STRING: - renderer = gtk_cell_renderer_text_new(); + if (cheat_type == CHEAT_TYPE_INTERNAL) + renderer = gtk_cell_renderer_text_new(); + else if (cheat_type == CHEAT_TYPE_AR) + renderer = desmume_cell_renderer_ndtext_new(); g_object_set(renderer, "editable", TRUE, NULL); g_signal_connect(renderer, "edited", - G_CALLBACK(cheat_list_modify_cheat), store); + G_CALLBACK(cheat_list_modify_cheat), model); attrib = "text"; break; case TYPE_COMBO: @@ -257,20 +338,29 @@ static void cheat_list_add_columns(GtkTreeView * tree, GtkListStore * store) NULL); g_object_unref(size_model); g_signal_connect(renderer, "edited", - G_CALLBACK(cheat_list_modify_cheat), store); + G_CALLBACK(cheat_list_modify_cheat), model); attrib = "text"; break; } + gint c = columnTable[ii].column; column = gtk_tree_view_column_new_with_attributes(columnTable[ii]. caption, renderer, - attrib, columnTable[ii].column, + attrib, c, NULL); - g_object_set_data(G_OBJECT(renderer), "column", - GINT_TO_POINTER(columnTable[ii].column)); - gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + if (c == COLUMN_HI && cheat_type == CHEAT_TYPE_INTERNAL) { + gtk_tree_view_column_set_cell_data_func( + column, renderer, cheat_list_address_to_hex, NULL, NULL); + } + if (c == COLUMN_ENABLED || c == COLUMN_DESC || + ((c == COLUMN_SIZE || c == COLUMN_HI || c == COLUMN_LO) && + cheat_type == CHEAT_TYPE_INTERNAL) || + (c == COLUMN_AR && cheat_type == CHEAT_TYPE_AR)) { + g_object_set_data(G_OBJECT(renderer), "column", + GINT_TO_POINTER(columnTable[ii].column)); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + } } - } static void cheatListEnd() @@ -282,8 +372,11 @@ static void cheatListEnd() static GtkListStore *cheat_list_populate() { - GtkListStore *store = gtk_list_store_new (5, G_TYPE_BOOLEAN, - G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING); + // COLUMN_INDEX, COLUMN_TYPE, COLUMN_ENABLED, COLUMN_SIZE, + // COLUMN_HI, COLUMN_LO, COLUMN_AR, COLUMN_DESC + GtkListStore *store = gtk_list_store_new ( + 8, G_TYPE_INT, G_TYPE_INT, G_TYPE_BOOLEAN, G_TYPE_INT, + G_TYPE_INT, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING); CHEATS_LIST cheat; u32 chsize = cheats->getListSize(); @@ -291,45 +384,113 @@ static GtkListStore *cheat_list_populate() GtkTreeIter iter; cheats->copyItemFromIndex(ii, cheat); gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, - COLUMN_ENABLED, cheat.enabled, - COLUMN_SIZE, cheat.size+1, - COLUMN_HI, cheat.code[0][0], - COLUMN_LO, cheat.code[0][1], - COLUMN_DESC, cheat.description, - -1); + if (cheat.type == CHEAT_TYPE_INTERNAL) { + gtk_list_store_set(store, &iter, COLUMN_INDEX, ii, + COLUMN_TYPE, cheat.type, + COLUMN_ENABLED, cheat.enabled, + COLUMN_SIZE, cheat.size + 1, + COLUMN_HI, cheat.code[0][0], + COLUMN_LO, cheat.code[0][1], + COLUMN_DESC, cheat.description, -1); + } else if (cheat.type == CHEAT_TYPE_AR) { + u32 cheat_len = cheat.num; + char *cheat_str = (char *) malloc(18 * cheat_len); + cheat_str[0] = '\0'; + + // Safety: "%08X" is 8 bytes (x2), " " and "\n" are 1 each for 18 + // bytes each strdup_printf called cheat_len times for the size of + // the malloc. g_strlcat emulates BSD's strlcat, so on the last + // iteration, a NUL-terminator is writted instead of the last + // trailing newline. + for (u32 jj = 0; jj < cheat_len; jj++) { + gchar *tmp = g_strdup_printf("%08X %08X\n", cheat.code[jj][0], + cheat.code[jj][1]); + g_strlcat(cheat_str, tmp, 18 * cheat_len); + g_free(tmp); + } + + gtk_list_store_set(store, &iter, COLUMN_INDEX, ii, + COLUMN_TYPE, cheat.type, + COLUMN_ENABLED, cheat.enabled, + COLUMN_AR, cheat_str, + COLUMN_DESC, cheat.description, -1); + free(cheat_str); + } } return store; } -static GtkWidget *cheat_list_create_ui() +static gboolean cheat_list_is_raw(GtkTreeModel *model, GtkTreeIter *iter, + gpointer data) +{ + gint type = CHEAT_TYPE_EMPTY; + gtk_tree_model_get(model, iter, COLUMN_TYPE, &type, -1); + return type == CHEAT_TYPE_INTERNAL; +} + +static gboolean cheat_list_is_ar(GtkTreeModel *model, GtkTreeIter *iter, + gpointer data) +{ + gint type = CHEAT_TYPE_EMPTY; + gtk_tree_model_get(model, iter, COLUMN_TYPE, &type, -1); + return type == CHEAT_TYPE_AR; +} + +static void cheat_list_create_ui() { GtkListStore *store = cheat_list_populate(); - GtkWidget *tree = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store)); + GtkTreeModel *filter_raw = + gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL); + gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filter_raw), + cheat_list_is_raw, NULL, NULL); + GtkWidget *tree_raw = gtk_tree_view_new_with_model(filter_raw); + GtkWidget *vbox = gtk_vbox_new(FALSE, 1); GtkWidget *hbbox = gtk_hbutton_box_new(); GtkWidget *button; - - gtk_container_add(GTK_CONTAINER(vbox), GTK_WIDGET(tree)); + + gtk_container_add(GTK_CONTAINER(vbox), GTK_WIDGET(tree_raw)); gtk_container_add(GTK_CONTAINER(vbox), GTK_WIDGET(hbbox)); gtk_container_add(GTK_CONTAINER(win), GTK_WIDGET(vbox)); - button = gtk_button_new_with_label("add cheat"); + button = gtk_button_new_with_label("Add internal cheat"); g_signal_connect (button, "clicked", G_CALLBACK (cheat_list_add_cheat), store); gtk_container_add(GTK_CONTAINER(hbbox),button); - button = gtk_button_new_with_label("Remove cheat"); - g_signal_connect (button, "clicked", G_CALLBACK (cheat_list_remove_cheat), tree); + button = gtk_button_new_with_label("Remove internal cheat"); + g_signal_connect (button, "clicked", G_CALLBACK (cheat_list_remove_cheat), + GTK_TREE_VIEW(tree_raw)); gtk_container_add(GTK_CONTAINER(hbbox),button); - cheat_list_add_columns(GTK_TREE_VIEW(tree), store); - + GtkTreeModel *filter_ar = + gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL); + gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filter_ar), + cheat_list_is_ar, NULL, NULL); + GtkWidget *tree_ar = gtk_tree_view_new_with_model(filter_ar); + + hbbox = gtk_hbutton_box_new(); + gtk_container_add(GTK_CONTAINER(vbox), GTK_WIDGET(tree_ar)); + gtk_container_add(GTK_CONTAINER(vbox), GTK_WIDGET(hbbox)); + + button = gtk_button_new_with_label("Add Action Replay cheat"); + gtk_container_add(GTK_CONTAINER(hbbox), button); + g_signal_connect(button, "clicked", G_CALLBACK(cheat_list_add_cheat_AR), + store); + + button = gtk_button_new_with_label("Remove Action Replay cheat"); + g_signal_connect(button, "clicked", G_CALLBACK(cheat_list_remove_cheat), + GTK_TREE_VIEW(tree_ar)); + gtk_container_add(GTK_CONTAINER(hbbox), button); + + cheat_list_add_columns(GTK_TREE_VIEW(tree_raw), store, CHEAT_TYPE_INTERNAL); + cheat_list_add_columns(GTK_TREE_VIEW(tree_ar), store, CHEAT_TYPE_AR); + /* Setup the selection handler */ GtkTreeSelection *select; - select = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree)); + select = gtk_tree_view_get_selection (GTK_TREE_VIEW(tree_raw)); + gtk_tree_selection_set_mode (select, GTK_SELECTION_SINGLE); + select = gtk_tree_view_get_selection (GTK_TREE_VIEW(tree_ar)); gtk_tree_selection_set_mode (select, GTK_SELECTION_SINGLE); - - return tree; } void CheatList () diff --git a/desmume/src/frontend/posix/gtk2/meson.build b/desmume/src/frontend/posix/gtk2/meson.build index ffa9285fb..48cf024bf 100644 --- a/desmume/src/frontend/posix/gtk2/meson.build +++ b/desmume/src/frontend/posix/gtk2/meson.build @@ -13,6 +13,7 @@ desmume_src = [ 'desmume.cpp', 'dToolsList.cpp', 'tools/ioregsView.cpp', + 'utilsGTK.cpp', 'cheatsGTK.cpp', 'main.cpp', ] diff --git a/desmume/src/frontend/posix/gtk2/utilsGTK.cpp b/desmume/src/frontend/posix/gtk2/utilsGTK.cpp new file mode 100644 index 000000000..e8ca5ce0b --- /dev/null +++ b/desmume/src/frontend/posix/gtk2/utilsGTK.cpp @@ -0,0 +1,355 @@ +/* utilsGTK.cpp - this file is part of DeSmuME + * + * Copyright (C) 2024 DeSmuME Team + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "utilsGTK.h" +#include +#include + +/* + A C++ implementation of a GtkCellRendererText subclass which handles + newline-delimited text and allows for editing that text with the ability + for users to add newlines, based off of a GPLv2+ Python implementation here: + https://gitlab.gnome.org/GNOME/gtk/-/issues/175#note_487323 +*/ + +/* + DESMUME_ENTRY_ND: + An object similar to an Entry, but which allows for newlines to be + inserted by holding Shift, Ctrl, or Alt along with pressing Enter. +*/ + +struct _DesmumeEntryNdPrivate +{ + GtkWidget *scroll; + GtkWidget *editor; + gboolean editing_canceled; +}; + +typedef enum +{ + PROP_EDITING_CANCELED = 1, + ENTRY_ND_NUM_PROP, +} DesmumeEntryNdProperty; + +static GParamSpec *entry_nd_properties[ENTRY_ND_NUM_PROP] = { + NULL, +}; + +// Declared here to statisfy the type creation macro, but defined further down. +static void desmume_entry_nd_cell_editable_init(GtkCellEditableIface *iface); + +// As defined in GObject 2.38, which is past the last release of GTK2. +// https://gitlab.gnome.org/GNOME/glib/-/blob/main/gobject/gtype.h#L2188 +#ifndef G_ADD_PRIVATE +#define G_ADD_PRIVATE(TypeName) \ + { \ + TypeName##_private_offset = g_type_add_instance_private( \ + g_define_type_id, sizeof(TypeName##Private)); \ + } +#endif + +G_DEFINE_TYPE_WITH_CODE(DesmumeEntryNd, desmume_entry_nd, GTK_TYPE_EVENT_BOX, + G_ADD_PRIVATE(DesmumeEntryNd) G_IMPLEMENT_INTERFACE( + GTK_TYPE_CELL_EDITABLE, + desmume_entry_nd_cell_editable_init)) + +static void desmume_entry_nd_set_property(GObject *object, guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + DesmumeEntryNd *entry_nd = DESMUME_ENTRY_ND(object); + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + switch ((DesmumeEntryNdProperty) property_id) { + case PROP_EDITING_CANCELED: + if (priv->editing_canceled != g_value_get_boolean(value)) { + priv->editing_canceled = g_value_get_boolean(value); + g_object_notify(object, "editing-canceled"); + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void desmume_entry_nd_get_property(GObject *object, guint property_id, + GValue *value, GParamSpec *pspec) +{ + DesmumeEntryNd *entry_nd = DESMUME_ENTRY_ND(object); + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + switch ((DesmumeEntryNdProperty) property_id) { + case PROP_EDITING_CANCELED: + g_value_set_boolean(value, priv->editing_canceled); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +#define GDK_EVENT_PROPAGATE FALSE +#define GDK_EVENT_STOP TRUE +static gboolean desmume_entry_nd_key_press(GtkWidget *widget, + GdkEventKey *event, gpointer data) +{ + DesmumeEntryNd *entry_nd = (DesmumeEntryNd *) data; + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + // Allow the editor to decide how to handle the key event, except key events + // which its parent TextView needs to handle itself + gboolean doPropagate = GDK_EVENT_PROPAGATE; + guint kv = event->keyval; + guint mod = event->state; + + if ((kv == GDK_Return || kv == GDK_KP_Enter || kv == GDK_ISO_Enter) && + !(mod & (GDK_CONTROL_MASK | GDK_SHIFT_MASK | GDK_MOD1_MASK))) { + // Enter + Ctrl, Shift, or Mod1 (commonly Alt), enter a newline in the + // editor, but otherwise act as confirm for the TextView + priv->editing_canceled = FALSE; + doPropagate = GDK_EVENT_STOP; + gtk_cell_editable_editing_done(GTK_CELL_EDITABLE(entry_nd)); + } else if (kv == GDK_Escape) { + priv->editing_canceled = TRUE; + doPropagate = GDK_EVENT_STOP; + gtk_cell_editable_editing_done(GTK_CELL_EDITABLE(entry_nd)); + } + return doPropagate; +} + +static gboolean desmume_entry_nd_button_press(GtkWidget *widget, + GdkEventButton *event, + gpointer data) +{ + GtkTextView *editor = (GtkTextView *) widget; + GtkWidgetClass *klass = GTK_WIDGET_GET_CLASS(editor); + klass->button_press_event(widget, event); + // We have explicitly described how to handle mouse button events, so do not + // propagate. + return GDK_EVENT_STOP; +} +#undef GDK_EVENT_PROPAGATE +#undef GDK_EVENT_STOP + +static void desmume_entry_nd_dispose(GObject *object) +{ + DesmumeEntryNd *entry_nd = DESMUME_ENTRY_ND(object); + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + // Recursively destroys contained objects, so destroys the editor as well + gtk_widget_destroy(priv->scroll); + + G_OBJECT_CLASS(desmume_entry_nd_parent_class)->dispose(object); +} + +static void desmume_entry_nd_finalize(GObject *object) +{ + G_OBJECT_CLASS(desmume_entry_nd_parent_class)->finalize(object); +} + +static void desmume_entry_nd_size_request(GtkWidget *widget, + GtkRequisition *req) +{ + DesmumeEntryNd *entry_nd = DESMUME_ENTRY_ND(widget); + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + gtk_widget_size_request(GTK_WIDGET(priv->scroll), req); + GtkRequisition *temp_req = gtk_requisition_copy(req); + gtk_widget_size_request(GTK_WIDGET(priv->editor), req); + req->width += temp_req->width; + req->height += temp_req->height; + gtk_requisition_free(temp_req); +} + +static void desmume_entry_nd_class_init(DesmumeEntryNdClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + object_class->set_property = desmume_entry_nd_set_property; + object_class->get_property = desmume_entry_nd_get_property; + object_class->dispose = desmume_entry_nd_dispose; + object_class->finalize = desmume_entry_nd_finalize; + + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + widget_class->size_request = desmume_entry_nd_size_request; + + entry_nd_properties[PROP_EDITING_CANCELED] = + g_param_spec_boolean("editing-canceled", "Editing Canceled", + "The edit was canceled", FALSE, G_PARAM_READWRITE); + g_object_class_install_properties(object_class, ENTRY_ND_NUM_PROP, + entry_nd_properties); +} + +static void desmume_entry_nd_init(DesmumeEntryNd *entry_nd) +{ + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + priv->scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(priv->scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + + priv->editor = gtk_text_view_new(); + gtk_text_view_set_editable(GTK_TEXT_VIEW(priv->editor), TRUE); + gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(priv->editor), FALSE); + + g_signal_connect(priv->editor, "key-press-event", + G_CALLBACK(desmume_entry_nd_key_press), entry_nd); + g_signal_connect(priv->editor, "button-press-event", + G_CALLBACK(desmume_entry_nd_button_press), NULL); + + gtk_container_add(GTK_CONTAINER(priv->scroll), priv->editor); + gtk_container_add(GTK_CONTAINER(entry_nd), priv->scroll); +} + +static void desmume_entry_nd_start_editing(GtkCellEditable *cell_editable, + GdkEvent *event) +{ + DesmumeEntryNd *entry_nd = DESMUME_ENTRY_ND(cell_editable); + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + gtk_widget_show_all(GTK_WIDGET(entry_nd)); + gtk_widget_grab_focus(GTK_WIDGET(priv->editor)); + + // Highlight the entirety of the editor's text + GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->editor)); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buf, &start, &end); + gtk_text_buffer_select_range(buf, &start, &end); +} + +static gchar *desmume_entry_nd_get_text(DesmumeEntryNd *entry_nd) +{ + DesmumeEntryNdPrivate *priv = + (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->editor)); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buf, &start, &end); + + return gtk_text_buffer_get_text(buf, &start, &end, TRUE); +} + +static void desmume_entry_nd_set_text(DesmumeEntryNd *entry_nd, + const gchar *text) +{ + DesmumeEntryNdPrivate *priv; + priv = (DesmumeEntryNdPrivate *) desmume_entry_nd_get_instance_private( + entry_nd); + + GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->editor)); + gtk_text_buffer_set_text(buf, text, strlen(text)); +} + +static void desmume_entry_nd_cell_editable_init(GtkCellEditableIface *iface) +{ + iface->start_editing = desmume_entry_nd_start_editing; +} + +GtkWidget *desmume_entry_nd_new() +{ + return (GtkWidget *) g_object_new(DESMUME_TYPE_ENTRY_ND, NULL); +} + +/* + DESMUME_CELL_RENDERER_ND_TEXT: + A subclass of GtkCellRendererText which creates our DesmumeEntryNd instead + of a GtkEntry, which allows a cell in a TreeView to accepts newlines. +*/ + +G_DEFINE_TYPE(DesmumeCellRendererNdtext, desmume_cell_renderer_ndtext, + GTK_TYPE_CELL_RENDERER_TEXT) + +static void desmume_cell_renderer_ndtext_editing_done(GtkCellEditable *entry_nd, + gpointer data) +{ + gboolean canceled; + g_object_get(entry_nd, "editing-canceled", &canceled, NULL); + if (!canceled) { + const gchar *path = + (gchar *) g_object_get_data(G_OBJECT(entry_nd), "full-text"); + gchar *new_text = desmume_entry_nd_get_text(DESMUME_ENTRY_ND(entry_nd)); + + guint signal_id = + g_signal_lookup("edited", DESMUME_TYPE_CELL_RENDERER_NDTEXT); + g_signal_emit(data, signal_id, 0, path, new_text); + g_free(new_text); + } + gtk_cell_editable_remove_widget(GTK_CELL_EDITABLE(entry_nd)); +} + +static GtkCellEditable *desmume_cell_renderer_ndtext_start_editing( + GtkCellRenderer *cell, GdkEvent *event, GtkWidget *widget, + const gchar *path, GdkRectangle *background_area, GdkRectangle *cell_area, + GtkCellRendererState flags) +{ + DesmumeCellRendererNdtext *ndtext = DESMUME_CELL_RENDERER_NDTEXT(cell); + gboolean editable; + g_object_get(G_OBJECT(ndtext), "editable", &editable, NULL); + if (!editable) + return NULL; + + gchar *text; + g_object_get(G_OBJECT(ndtext), "text", &text, NULL); + + GtkWidget *entry_nd = desmume_entry_nd_new(); + + if (text != NULL) { + desmume_entry_nd_set_text(DESMUME_ENTRY_ND(entry_nd), text); + g_free(text); + } + g_object_set_data_full(G_OBJECT(entry_nd), "full-text", g_strdup(path), + g_free); + + g_signal_connect(entry_nd, "editing-done", + G_CALLBACK(desmume_cell_renderer_ndtext_editing_done), + ndtext); + return GTK_CELL_EDITABLE(entry_nd); +} + +static void +desmume_cell_renderer_ndtext_class_init(DesmumeCellRendererNdtextClass *klass) +{ + GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS(klass); + cell_class->start_editing = desmume_cell_renderer_ndtext_start_editing; +} + +static void desmume_cell_renderer_ndtext_init(DesmumeCellRendererNdtext *ndtext) +{ +} + +GtkCellRenderer *desmume_cell_renderer_ndtext_new() +{ + return GTK_CELL_RENDERER( + g_object_new(DESMUME_TYPE_CELL_RENDERER_NDTEXT, NULL)); +} diff --git a/desmume/src/frontend/posix/gtk2/utilsGTK.h b/desmume/src/frontend/posix/gtk2/utilsGTK.h new file mode 100644 index 000000000..a39b94443 --- /dev/null +++ b/desmume/src/frontend/posix/gtk2/utilsGTK.h @@ -0,0 +1,92 @@ +/* utilsGTK.h - this file is part of DeSmuME + * + * Copyright (C) 2024 DeSmuME Team + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __UTILS_GTK_H__ +#define __UTILS_GTK_H__ + +#include + +G_BEGIN_DECLS + +#define DESMUME_TYPE_ENTRY_ND (desmume_entry_nd_get_type()) +#define DESMUME_ENTRY_ND(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), DESMUME_TYPE_ENTRY_ND, DesmumeEntryNd)) +#define DESMUME_ENTRY_ND_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), DESMUME_TYPE_ENTRY_ND, DesmumeEntryNdClass)) +#define DESMUME_IS_ENTRY_ND(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), DESMUME_TYPE_ENTRY_ND)) +#define DESMUME_IS_ENTRY_ND_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), DESMUME_TYPE_ENTRY_ND)) +#define DESMUME_ENTRY_ND_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), DESMUME_TYPE_ENTRY_ND, DesmumeEntryNdClass)) + +typedef struct _DesmumeEntryNd DesmumeEntryNd; +typedef struct _DesmumeEntryNdClass DesmumeEntryNdClass; +typedef struct _DesmumeEntryNdPrivate DesmumeEntryNdPrivate; + +struct _DesmumeEntryNd +{ + GtkEventBox parent; + DesmumeEntryNdPrivate *priv; +}; + +struct _DesmumeEntryNdClass +{ + GtkEventBoxClass parent_class; +}; + +GType desmume_entry_nd_get_type(void) G_GNUC_CONST; +GtkEventBox *entry_nd_new(void); + +#define DESMUME_TYPE_CELL_RENDERER_NDTEXT \ + (desmume_cell_renderer_ndtext_get_type()) +#define DESMUME_CELL_RENDERER_NDTEXT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), DESMUME_TYPE_CELL_RENDERER_NDTEXT, \ + DesmumeCellRendererNdtext)) +#define DESMUME_CELL_RENDERER_NDTEXT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST( \ + (klass), DESMUME_TYPE_CELL_RENDERER_NDTEXT DesmumeCellRendererNdtextClass)) +#define DESMUME_IS_CELL_RENDERER_NDTEXT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), DESMUME_TYPE_CELL_RENDERER_NDTEXT)) +#define DESMUME_IS_CELL_RENDERER_NDTEXT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), DESMUME_TYPE_CELL_RENDERER_NDTEXT)) +#define DESMUME_CELL_RENDERER_NDTEXT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS(obj), DESMUME_TYPE_CELL_RENDERER_NDTEXT, \ + DesmumeCellRendererNdtextClass) + +typedef struct _DesmumeCellRendererNdtext DesmumeCellRendererNdtext; +typedef struct _DesmumeCellRendererNdtextClass DesmumeCellRendererNdtextClass; + +struct _DesmumeCellRendererNdtext +{ + GtkCellRendererText parent; +}; + +struct _DesmumeCellRendererNdtextClass +{ + GtkCellRendererTextClass parent_class; +}; + +GType desmume_cell_renderer_ndtext_get_type(void) G_GNUC_CONST; +GtkCellRenderer *desmume_cell_renderer_ndtext_new(void); + +G_END_DECLS + +#endif /*__UTILS_GTK_H__*/