Skip to content

Commit

Permalink
[gtk] ListDataItem, AchievementItem: Use boxed GPtrArray/GDateTime fo…
Browse files Browse the repository at this point in the history
…r some properties.

ListDataItem: Use a GPtrArray for `const char*` instead of a
manually-allocated array. Column count is now determined by
checking the GPtrArray length. The entire GPtrArray can be obtained
and replaced, but it's not recommended to do so.

AchievementItem: Use a GDateTime for unlock_time. NULL is used to
represent an invalid timestamp instead of -1 here. Keeping a GDateTime
instead of a time_t increases memory usage slightly, but it reduces
overhead because we don't have to keep converting from time_t to
GDateTime every time the timestamp is requested.

Note that we can't use GDateTime like this in the GTK2/GTK3 version
because there's no GtkCellRenderer that takes GDateTime. In theory,
we could make our own, but I'd rather not do that.

Use g_clear_pointer() to unreference objects in the dispose() functions.
  • Loading branch information
GerbilSoft committed Sep 14, 2024
1 parent 755b9e0 commit 524a231
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 66 deletions.
101 changes: 72 additions & 29 deletions src/gtk/ListDataItem.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ typedef enum {
PROP_ICON,
PROP_CHECKED,
PROP_COLUMN_COUNT,
// NOTE: Column text is not a property.
PROP_COLUMN_TEXT,

PROP_LAST
} RpAchievementPropID;
Expand Down Expand Up @@ -49,9 +49,7 @@ struct _RpListDataItem {
PIMGTYPE icon;
RpListDataItemCol0Type col0_type;
gboolean checked;

int column_count;
char **text;
GPtrArray *text;
};

// NOTE: G_DEFINE_TYPE() doesn't work in C++ mode with gcc-6.2
Expand Down Expand Up @@ -90,6 +88,14 @@ rp_list_data_item_class_init(RpListDataItemClass *klass)
1, 16, 1,
(GParamFlags)(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY));

// Technically read/write, but callers should use the
// convenience functions to edit individual strings
// instead of getting the GPtrArray object pointer.
props[PROP_COLUMN_TEXT] = g_param_spec_boxed(
"column-text", "Column Text", "Array of column text",
G_TYPE_PTR_ARRAY,
(GParamFlags)(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY));

// Install the properties.
g_object_class_install_properties(gobject_class, PROP_LAST, props);
}
Expand All @@ -108,11 +114,13 @@ rp_list_data_item_new(int column_count, RpListDataItemCol0Type col0_type)
RpListDataItem *const item = g_object_new(RP_TYPE_LIST_DATA_ITEM, NULL);

// Setting column count manually because set_property() won't set it.
item->column_count = column_count;
item->col0_type = col0_type;

// Allocate the string array.
item->text = calloc(column_count, sizeof(*item->text));
item->text = g_ptr_array_new_full(column_count, g_free);
for (int i = 0; i < column_count; i++) {
g_ptr_array_add(item->text, NULL);
}

return item;
}
Expand All @@ -136,6 +144,10 @@ rp_list_data_item_set_property(GObject *object,
rp_list_data_item_set_checked(item, g_value_get_boolean(value));
break;

case PROP_COLUMN_TEXT:
rp_list_data_item_set_column_text_array(item, (GPtrArray*)g_value_get_boxed(value));
break;

// TODO: Handling read-only properties?
case PROP_COL0_TYPE:
case PROP_COLUMN_COUNT:
Expand Down Expand Up @@ -170,7 +182,11 @@ rp_list_data_item_get_property(GObject *object,
break;

case PROP_COLUMN_COUNT:
g_value_set_int(value, item->column_count);
g_value_set_int(value, (item->text ? item->text->len : 0));
break;

case PROP_COLUMN_TEXT:
g_value_set_boxed(value, item->text);
break;

default:
Expand All @@ -186,10 +202,8 @@ rp_list_data_item_dispose(GObject *object)
{
RpListDataItem *const item = RP_LIST_DATA_ITEM(object);

if (item->icon) {
PIMGTYPE_unref(item->icon);
item->icon = NULL;
}
g_clear_pointer(&item->icon, PIMGTYPE_unref);
g_clear_pointer(&item->text, g_ptr_array_unref);

// Call the superclass dispose() function.
G_OBJECT_CLASS(rp_list_data_item_parent_class)->dispose(object);
Expand All @@ -199,17 +213,6 @@ rp_list_data_item_dispose(GObject *object)
static void
rp_list_data_item_finalize(GObject *object)
{
RpListDataItem *const item = RP_LIST_DATA_ITEM(object);

if (item->text) {
// Free all of the strings first.
for (int i = 0; i < item->column_count; i++) {
g_free(item->text[i]);
}
g_free(item->text);
item->text = NULL;
}

// Call the superclass finalize() function.
G_OBJECT_CLASS(rp_list_data_item_parent_class)->finalize(object);
}
Expand Down Expand Up @@ -268,25 +271,65 @@ rp_list_data_item_get_checked(RpListDataItem *item)
return item->checked;
}

void
rp_list_data_item_set_column_text_array(RpListDataItem *item, GPtrArray *text)
{
g_return_if_fail(RP_IS_LIST_DATA_ITEM(item));

if (item->text == text) {
// Same array...
return;
}

const uint column_count_old = item->text->len;
uint column_count_new;
if (item->text) {
g_ptr_array_unref(item->text);
}
if (text) {
item->text = g_ptr_array_ref(text);
column_count_new = text->len;
} else {
// No text array...
item->text = NULL;
column_count_new = 0;
}

if (column_count_new != column_count_old) {
g_object_notify_by_pspec(G_OBJECT(item), props[PROP_COLUMN_COUNT]);
}
g_object_notify_by_pspec(G_OBJECT(item), props[PROP_COLUMN_TEXT]);
}

GPtrArray*
rp_list_data_item_get_column_text_array(RpListDataItem *item)
{
g_return_val_if_fail(RP_IS_LIST_DATA_ITEM(item), NULL);
return item->text;
}

void
rp_list_data_item_set_column_text(RpListDataItem *item, int column, const char *text)
{
g_return_if_fail(RP_IS_LIST_DATA_ITEM(item));
g_return_if_fail(column >= 0 && column < item->column_count);
g_return_if_fail(item->text != NULL);
g_return_if_fail(column >= 0 && column < (int)item->text->len);

if (item->text[column]) {
g_free(item->text[column]);
if (item->text->pdata[column]) {
g_free(item->text->pdata[column]);
}
item->text[column] = g_strdup(text);
// TODO: Signal that text has changed?
item->text->pdata[column] = g_strdup(text);
// TODO: Signal that only a single column has changed?
g_object_notify_by_pspec(G_OBJECT(item), props[PROP_COLUMN_TEXT]);
}

const char*
rp_list_data_item_get_column_text(RpListDataItem *item, int column)
{
g_return_val_if_fail(RP_IS_LIST_DATA_ITEM(item), NULL);
g_return_val_if_fail(column >= 0 && column < item->column_count, NULL);
g_return_val_if_fail(item->text != NULL, NULL);
g_return_val_if_fail(column >= 0 && column < (int)item->text->len, NULL);

// String is owned by this object.
return item->text[column];
return item->text->pdata[column];
}
3 changes: 3 additions & 0 deletions src/gtk/ListDataItem.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ gboolean rp_list_data_item_get_checked (RpListDataItem *item);

int rp_list_data_item_get_column_count (RpListDataItem *item);

void rp_list_data_item_set_column_text_array (RpListDataItem *item, GPtrArray *text);
GPtrArray* rp_list_data_item_get_column_text_array (RpListDataItem *item);

void rp_list_data_item_set_column_text (RpListDataItem *item, int column, const char *text);
const char* rp_list_data_item_get_column_text (RpListDataItem *item, int column); // owned by this object

Expand Down
3 changes: 3 additions & 0 deletions src/gtk/RomDataView_gtk4.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,12 @@ bind_listitem_cb(GtkListItemFactory *factory, GtkListItem *list_item, gpointer u
default:
assert(!"Unsupported col0_type!");
return;

case RP_LIST_DATA_ITEM_COL0_TYPE_TEXT:
// No icon or checkbox.
gtk_label_set_markup(GTK_LABEL(widget), rp_list_data_item_get_column_text(item, column));
break;

case RP_LIST_DATA_ITEM_COL0_TYPE_CHECKBOX:
// Column 0 is a checkbox.
if (column == 0) {
Expand All @@ -131,6 +133,7 @@ bind_listitem_cb(GtkListItemFactory *factory, GtkListItem *list_item, gpointer u
gtk_label_set_markup(GTK_LABEL(widget), rp_list_data_item_get_column_text(item, column-1));
}
break;

case RP_LIST_DATA_ITEM_COL0_TYPE_ICON:
// Column 0 is an icon.
if (column == 0) {
Expand Down
50 changes: 30 additions & 20 deletions src/gtk/config/AchievementItem.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,7 @@ struct _RpAchievementItem {

PIMGTYPE icon;
char *description;

// NOTE: GObject's property system doesn't appear to support GDateTime.
// The property will be gint64 without GDateTime support.
gint64 unlock_time;
GDateTime *unlock_time;
};

// NOTE: G_DEFINE_TYPE() doesn't work in C++ mode with gcc-6.2
Expand Down Expand Up @@ -77,9 +74,9 @@ rp_achievement_item_class_init(RpAchievementItemClass *klass)
"",
(GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY));

props[PROP_UNLOCK_TIME] = g_param_spec_int64(
props[PROP_UNLOCK_TIME] = g_param_spec_boxed(
"unlock-time", "Unlock Time", "Timestamp when this achievement was unlocked",
G_MININT64, G_MAXINT64, -1LL,
G_TYPE_DATE_TIME,
(GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY));

// Install the properties.
Expand All @@ -93,12 +90,12 @@ rp_achievement_item_init(RpAchievementItem *item)
}

RpAchievementItem*
rp_achievement_item_new(PIMGTYPE icon, const char *description, time_t unlock_time)
rp_achievement_item_new(PIMGTYPE icon, const char *description, GDateTime *unlock_time)
{
return g_object_new(RP_TYPE_ACHIEVEMENT_ITEM,
"icon", icon,
"description", description,
"unlock-time", (gint64)unlock_time,
"unlock-time", unlock_time,
NULL);
}

Expand All @@ -122,8 +119,7 @@ rp_achievement_item_set_property(GObject *object,
break;

case PROP_UNLOCK_TIME:
// FIXME: May cause truncation on systems where time_t is still 32-bit.
rp_achievement_item_set_unlock_time(item, g_value_get_int64(value));
rp_achievement_item_set_unlock_time(item, (GDateTime*)g_value_get_boxed(value));
break;

default:
Expand Down Expand Up @@ -153,7 +149,7 @@ rp_achievement_item_get_property(GObject *object,
break;

case PROP_UNLOCK_TIME:
g_value_set_int64(value, item->unlock_time);
g_value_set_boxed(value, item->unlock_time);
break;

default:
Expand All @@ -169,10 +165,8 @@ rp_achievement_item_dispose(GObject *object)
{
RpAchievementItem *const item = RP_ACHIEVEMENT_ITEM(object);

if (item->icon) {
PIMGTYPE_unref(item->icon);
item->icon = NULL;
}
g_clear_pointer(&item->icon, PIMGTYPE_unref);
g_clear_pointer(&item->unlock_time, g_date_time_unref);

// Call the superclass dispose() function.
G_OBJECT_CLASS(rp_achievement_item_parent_class)->dispose(object);
Expand Down Expand Up @@ -240,19 +234,35 @@ rp_achievement_item_get_description(RpAchievementItem *item)
}

void
rp_achievement_item_set_unlock_time(RpAchievementItem *item, time_t unlock_time)
rp_achievement_item_set_unlock_time(RpAchievementItem *item, GDateTime *unlock_time)
{
g_return_if_fail(RP_IS_ACHIEVEMENT_ITEM(item));

if (item->unlock_time != (gint64)unlock_time) {
item->unlock_time = (gint64)unlock_time;
if (item->unlock_time == unlock_time) {
// Same GDateTime struct.
return;
} else if (!item->unlock_time) {
// We don't have a GDateTime struct right now.
item->unlock_time = g_date_time_ref(unlock_time);
g_object_notify_by_pspec(G_OBJECT(item), props[PROP_UNLOCK_TIME]);
return;
}

// Free the current GDateTime struct.
g_date_time_unref(item->unlock_time);
if (unlock_time) {
// Reference the new GDateTime struct.
item->unlock_time = g_date_time_ref(unlock_time);
} else {
// New GDateTime struct is NULL.
item->unlock_time = NULL;
}
g_object_notify_by_pspec(G_OBJECT(item), props[PROP_UNLOCK_TIME]);
}

time_t
GDateTime*
rp_achievement_item_get_unlock_time(RpAchievementItem *item)
{
g_return_val_if_fail(RP_IS_ACHIEVEMENT_ITEM(item), -1);
g_return_val_if_fail(RP_IS_ACHIEVEMENT_ITEM(item), NULL);
return item->unlock_time;
}
6 changes: 3 additions & 3 deletions src/gtk/config/AchievementItem.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ G_BEGIN_DECLS
#define RP_TYPE_ACHIEVEMENT_ITEM (rp_achievement_item_get_type())
G_DECLARE_FINAL_TYPE(RpAchievementItem, rp_achievement_item, RP, ACHIEVEMENT_ITEM, GObject)

RpAchievementItem *rp_achievement_item_new (PIMGTYPE icon, const char *description, time_t unlock_time) G_GNUC_MALLOC;
RpAchievementItem *rp_achievement_item_new (PIMGTYPE icon, const char *description, GDateTime *unlock_time) G_GNUC_MALLOC;

void rp_achievement_item_set_icon (RpAchievementItem *item, PIMGTYPE icon);
PIMGTYPE rp_achievement_item_get_icon (RpAchievementItem *item);

void rp_achievement_item_set_description (RpAchievementItem *item, const char *description);
const char* rp_achievement_item_get_description (RpAchievementItem *item); // owned by this object

void rp_achievement_item_set_unlock_time (RpAchievementItem *item, time_t unlock_time);
time_t rp_achievement_item_get_unlock_time (RpAchievementItem *item);
void rp_achievement_item_set_unlock_time (RpAchievementItem *item, GDateTime *unlock_time);
GDateTime* rp_achievement_item_get_unlock_time (RpAchievementItem *item);

G_END_DECLS
29 changes: 15 additions & 14 deletions src/gtk/config/AchievementsTab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,19 +151,14 @@ bind_listitem_cb(GtkListItemFactory *factory, GtkListItem *list_item, gpointer u
// Unlock time
bool is_set = false;

const time_t unlock_time = rp_achievement_item_get_unlock_time(item);
const bool unlocked = (unlock_time != -1);
if (unlocked) {
GDateTime *const dateTime = g_date_time_new_from_unix_local(unlock_time);
assert(dateTime != nullptr);
if (dateTime) {
gchar *const str = g_date_time_format(dateTime, "%x %X");
if (str) {
gtk_label_set_text(GTK_LABEL(widget), str);
g_free(str);
is_set = true;
}
g_date_time_unref(dateTime);
// dateTime is NULL if the achievement is locked.
GDateTime *const dateTime = rp_achievement_item_get_unlock_time(item);
if (dateTime) {
gchar *const str = g_date_time_format(dateTime, "%x %X");
if (str) {
gtk_label_set_text(GTK_LABEL(widget), str);
g_free(str);
is_set = true;
}
}

Expand Down Expand Up @@ -249,6 +244,8 @@ rp_achievements_tab_init(RpAchievementsTab *tab)
}
#else /* !USE_GTK_COLUMN_VIEW */
// Create the GtkListStore and GtkTreeView.
// NOTE: Storing an already-formatted GDateTime as a string,
// since there is no GDateTime cell renderer.
tab->listStore = gtk_list_store_new(3, PIMGTYPE_GOBJECT_TYPE, G_TYPE_STRING, G_TYPE_STRING);
tab->treeView = gtk_tree_view_new_with_model(GTK_TREE_MODEL(tab->listStore));
gtk_widget_set_name(tab->treeView, "treeView");
Expand Down Expand Up @@ -351,7 +348,11 @@ rp_achievements_tab_reset(RpAchievementsTab *tab)

// Add the list item.
#ifdef USE_GTK_COLUMN_VIEW
RpAchievementItem *const item = rp_achievement_item_new(icon, s_ach.c_str(), timestamp);
GDateTime *const dateTime = (timestamp != -1) ? g_date_time_new_from_unix_local(timestamp) : NULL;
RpAchievementItem *const item = rp_achievement_item_new(icon, s_ach.c_str(), dateTime);
if (dateTime) {
g_date_time_unref(dateTime);
}
g_list_store_append(tab->listStore, item);
PIMGTYPE_unref(icon);
g_object_unref(item);
Expand Down

0 comments on commit 524a231

Please sign in to comment.