diff --git a/src/main.c b/src/main.c index ea5c7c9..5670b73 100644 --- a/src/main.c +++ b/src/main.c @@ -27,7 +27,7 @@ static void register_with_portal (void) { G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); if (result == NULL) { - g_info ("portal register: %s", error->message); + g_warning ("portal register: %s", error->message); g_clear_error (&error); } else { g_variant_unref (result); diff --git a/src/moemoji-application.c b/src/moemoji-application.c index 8d69fda..1a6d98d 100644 --- a/src/moemoji-application.c +++ b/src/moemoji-application.c @@ -301,8 +301,8 @@ static void setup_sni(MoeMojiApplication *self) { GVariant *result = g_dbus_connection_call_sync( self->dbus_conn, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameHasOwner", - g_variant_new("(s)", "org.kde.StatusNotifierWatcher"), G_VARIANT_TYPE("(b)"), - G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); + g_variant_new("(s)", "org.kde.StatusNotifierWatcher"), + G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); if (result == NULL) { g_warning("NameHasOwner check failed: %s", error->message); g_clear_error(&error); @@ -354,10 +354,10 @@ static void setup_sni(MoeMojiApplication *self) { } const gchar *unique_name = g_dbus_connection_get_unique_name(self->dbus_conn); g_dbus_connection_call( - self->dbus_conn, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", - "org.kde.StatusNotifierWatcher", "RegisterStatusNotifierItem", - g_variant_new("(s)", unique_name), NULL, G_DBUS_CALL_FLAGS_NONE, -1, - NULL, NULL, NULL); + self->dbus_conn, "org.kde.StatusNotifierWatcher", + "/StatusNotifierWatcher", "org.kde.StatusNotifierWatcher", + "RegisterStatusNotifierItem", g_variant_new("(s)", unique_name), NULL, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); } static void on_shortcuts_activated(G_GNUC_UNUSED GDBusProxy *proxy, @@ -593,8 +593,8 @@ static void moemoji_application_show_about(G_GNUC_UNUSED GSimpleAction *action, GtkWindow *window = NULL; g_return_if_fail(MOEMOJI_IS_APPLICATION(self)); window = gtk_application_get_active_window(GTK_APPLICATION(self)); - gtk_show_about_dialog(window, "program-name", "MoeMoji", "version", "0.1.0", - NULL); + gtk_show_about_dialog(window, "program-name", "MoeMoji", "version", + PACKAGE_VERSION, NULL); } static void moemoji_application_class_init(MoeMojiApplicationClass *klass) { diff --git a/src/moemoji-window.c b/src/moemoji-window.c index 58b62a0..7f2015e 100644 --- a/src/moemoji-window.c +++ b/src/moemoji-window.c @@ -2,10 +2,14 @@ #include "moemoji-config.h" #include "moemoji-internal.h" +#include +#include #include G_DEFINE_TYPE(MoeMojiWindow, moemoji_window, GTK_TYPE_APPLICATION_WINDOW) +static void reload_categories(MoeMojiWindow *self); + static void category_widgets_free(gpointer data) { CategoryWidgets *cw = data; g_free(cw->name); @@ -79,6 +83,9 @@ static void add_kaomoji_button(GtkFlowBox *flow, const char *text) { label_text = first_line; } GtkWidget *button = gtk_button_new_with_label(label_text); + GtkWidget *label = gtk_button_get_child(GTK_BUTTON(button)); + gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_END); + gtk_label_set_max_width_chars(GTK_LABEL(label), 20); gtk_widget_add_css_class(button, "kaomoji-button"); gtk_widget_add_css_class(button, "flat"); if (multiline || g_utf8_strlen(label_text, -1) > 20) @@ -106,6 +113,35 @@ static void add_kaomoji_button(GtkFlowBox *flow, const char *text) { gtk_flow_box_insert(flow, button, -1); } +static guint64 get_file_mtime(const char *path) { + GFile *f = g_file_new_for_path(path); + GFileInfo *info = g_file_query_info(f, G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + guint64 mtime = 0; + if (info) { + GDateTime *dt = g_file_info_get_modification_date_time(info); + if (dt) { + mtime = (guint64)g_date_time_to_unix(dt); + g_date_time_unref(dt); + } + g_object_unref(info); + } + g_object_unref(f); + return mtime; +} + +static gint compare_files_by_mtime(gconstpointer a, gconstpointer b) { + const char *pa = *(const char **)a; + const char *pb = *(const char **)b; + guint64 ma = get_file_mtime(pa); + guint64 mb = get_file_mtime(pb); + if (ma < mb) + return -1; + if (ma > mb) + return 1; + return 0; +} + static void load_category(MoeMojiWindow *self, const char *kaomoji_dir, const char *dirname) { char *cat_path = g_build_filename(kaomoji_dir, dirname, NULL); @@ -124,11 +160,16 @@ static void load_category(MoeMojiWindow *self, const char *kaomoji_dir, gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(flow), 2); gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(flow), 10); gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(flow), GTK_SELECTION_NONE); + GPtrArray *files = g_ptr_array_new_with_free_func(g_free); const char *filename; while ((filename = g_dir_read_name(dir)) != NULL) { if (!g_str_has_suffix(filename, ".txt")) continue; - char *filepath = g_build_filename(cat_path, filename, NULL); + g_ptr_array_add(files, g_build_filename(cat_path, filename, NULL)); + } + g_ptr_array_sort(files, compare_files_by_mtime); + for (guint i = 0; i < files->len; i++) { + const char *filepath = g_ptr_array_index(files, i); char *contents = NULL; if (g_file_get_contents(filepath, &contents, NULL, NULL)) { g_strchomp(contents); @@ -136,8 +177,8 @@ static void load_category(MoeMojiWindow *self, const char *kaomoji_dir, add_kaomoji_button(GTK_FLOW_BOX(flow), contents); g_free(contents); } - g_free(filepath); } + g_ptr_array_free(files, TRUE); gtk_box_append(self->content_box, flow); CategoryWidgets *cw = g_new0(CategoryWidgets, 1); cw->header = header; @@ -189,8 +230,8 @@ static void on_chip_clicked(GtkButton *button, gpointer user_data) { } static void update_chip_from_scroll(MoeMojiWindow *self) { - double scroll_pos = gtk_adjustment_get_value( - gtk_scrolled_window_get_vadjustment( + double scroll_pos = + gtk_adjustment_get_value(gtk_scrolled_window_get_vadjustment( GTK_SCROLLED_WINDOW(self->kaomoji_scroll))); int best = 0; for (guint i = 0; i < self->category_widgets->len; i++) { @@ -212,7 +253,7 @@ static void update_spacer_height(MoeMojiWindow *self) { GTK_SCROLLED_WINDOW(self->kaomoji_scroll)); int page = (int)gtk_adjustment_get_page_size(vadj); CategoryWidgets *last = g_ptr_array_index(self->category_widgets, - self->category_widgets->len - 1); + self->category_widgets->len - 1); int spacing = gtk_box_get_spacing(self->content_box); int last_h = gtk_widget_get_height(last->header) + spacing + gtk_widget_get_height(last->flow); @@ -229,14 +270,14 @@ static void on_scroll_changed(G_GNUC_UNUSED GtkAdjustment *adj, } static void on_page_size_changed(G_GNUC_UNUSED GtkAdjustment *adj, - G_GNUC_UNUSED GParamSpec *pspec, - gpointer user_data) { + G_GNUC_UNUSED GParamSpec *pspec, + gpointer user_data) { MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); update_spacer_height(self); } static void on_kaomoji_scroll_map(G_GNUC_UNUSED GtkWidget *widget, - gpointer user_data) { + gpointer user_data) { MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); update_chip_from_scroll(self); } @@ -257,20 +298,6 @@ static void on_search_changed(GtkSearchEntry *entry, gpointer user_data) { } } -static void moemoji_window_class_init(MoeMojiWindowClass *klass) { - GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); - gtk_widget_class_set_template_from_resource( - widget_class, "/jp/angeltech/MoeMoji/moemoji-window.ui"); - gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, outer_box); - gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, - content_box); - gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, - search_entry); - gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, header_bar); - gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, - kaomoji_scroll); -} - static void collect_categories(const char *base_dir, GHashTable *seen, GPtrArray *entries) { GDir *top = g_dir_open(base_dir, 0, NULL); @@ -300,17 +327,10 @@ static gint compare_entries(gconstpointer a, gconstpointer b) { return strcmp(ka, kb); } -static void moemoji_window_init(MoeMojiWindow *self) { - gtk_widget_init_template(GTK_WIDGET(self)); - gtk_widget_add_css_class(GTK_WIDGET(self), "wallpaper-bg"); - gtk_widget_add_css_class(GTK_WIDGET(self->header_bar), "wallpaper-bg"); - gtk_widget_add_css_class(GTK_WIDGET(self->content_box), "content-area"); - self->category_widgets = - g_ptr_array_new_with_free_func(category_widgets_free); - self->active_chip_index = -1; - char *kaomoji_dir = find_kaomoji_dir(); +static GPtrArray *collect_all_categories(void) { g_autofree char *user_dir = g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL); + char *kaomoji_dir = find_kaomoji_dir(); GHashTable *seen = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); GPtrArray *entries = g_ptr_array_new(); @@ -319,27 +339,244 @@ static void moemoji_window_init(MoeMojiWindow *self) { if (kaomoji_dir) collect_categories(kaomoji_dir, seen, entries); g_hash_table_destroy(seen); - if (entries->len == 0) { - GtkWidget *label = gtk_label_new("No kaomoji data found."); - gtk_box_append(self->content_box, label); - g_ptr_array_free(entries, TRUE); - g_free(kaomoji_dir); - return; - } g_ptr_array_sort(entries, compare_entries); + g_free(kaomoji_dir); + return entries; +} + +static void free_category_entries(GPtrArray *entries) { for (guint i = 0; i < entries->len; i++) { char **pair = g_ptr_array_index(entries, i); - load_category(self, pair[0], pair[1]); g_free(pair[0]); g_free(pair[1]); g_free(pair); } g_ptr_array_free(entries, TRUE); - g_free(kaomoji_dir); +} + +static void navigate_to(MoeMojiWindow *self, const char *page, + gboolean slide_left) { + gtk_stack_set_transition_type( + self->view_stack, slide_left ? GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT + : GTK_STACK_TRANSITION_TYPE_SLIDE_RIGHT); + gtk_stack_set_visible_child_name(self->view_stack, page); + gboolean is_main = (g_strcmp0(page, "main") == 0); + gtk_widget_set_visible(self->add_button, is_main); + gtk_widget_set_visible(self->back_button, !is_main); + gtk_widget_set_visible(self->menu_button, is_main); +} + +static void on_add_clicked(G_GNUC_UNUSED GtkButton *button, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + navigate_to(self, "add-choice", TRUE); +} + +static void on_back_clicked(G_GNUC_UNUSED GtkButton *button, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + navigate_to(self, "main", FALSE); +} + +static void on_new_category_clicked(G_GNUC_UNUSED GtkButton *button, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + gtk_editable_set_text(GTK_EDITABLE(self->category_name_entry), ""); + gtk_widget_set_sensitive(self->category_save_button, FALSE); + navigate_to(self, "add-category", TRUE); +} + +static void on_new_entry_clicked(G_GNUC_UNUSED GtkButton *button, + gpointer user_data); + +static gboolean is_valid_category_name(const char *text) { + if (!text || text[0] == '\0') + return FALSE; + gboolean has_non_space = FALSE; + for (const char *p = text; *p;) { + gunichar ch = g_utf8_get_char(p); + if (g_unichar_isalpha(ch) || g_unichar_isdigit(ch) || ch == '_' || + ch == '-') { + has_non_space = TRUE; + } else if (ch == ' ') { + /* space is allowed */ + } else { + return FALSE; + } + p = g_utf8_next_char(p); + } + return has_non_space; +} + +static char *normalize_category_name(const char *name) { + char *n = g_ascii_strdown(name, -1); + for (char *p = n; *p; p++) { + if (*p == ' ') + *p = '_'; + } + return n; +} + +static gboolean dir_has_category(const char *base, const char *norm_name) { + GDir *dir = g_dir_open(base, 0, NULL); + if (!dir) + return FALSE; + const char *name; + while ((name = g_dir_read_name(dir)) != NULL) { + g_autofree char *norm = normalize_category_name(name); + if (strcmp(norm, norm_name) == 0) { + g_autofree char *full = g_build_filename(base, name, NULL); + if (g_file_test(full, G_FILE_TEST_IS_DIR)) { + g_dir_close(dir); + return TRUE; + } + } + } + g_dir_close(dir); + return FALSE; +} + +static gboolean category_exists(const char *text) { + g_autofree char *stripped = g_strstrip(g_strdup(text)); + g_autofree char *norm = normalize_category_name(stripped); + g_autofree char *user_base = + g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL); + if (dir_has_category(user_base, norm)) + return TRUE; + char *bundled = find_kaomoji_dir(); + if (bundled) { + gboolean exists = dir_has_category(bundled, norm); + g_free(bundled); + if (exists) + return TRUE; + } + return FALSE; +} + +static void on_category_name_changed(G_GNUC_UNUSED GtkEditable *editable, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + const char *text = + gtk_editable_get_text(GTK_EDITABLE(self->category_name_entry)); + gtk_widget_set_sensitive(self->category_save_button, + is_valid_category_name(text) && + !category_exists(text)); +} + +static void on_category_save_clicked(G_GNUC_UNUSED GtkButton *button, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + const char *text = + gtk_editable_get_text(GTK_EDITABLE(self->category_name_entry)); + if (!is_valid_category_name(text) || category_exists(text)) + return; + g_autofree char *dir_name = g_strdup(text); + g_strstrip(dir_name); + for (char *p = dir_name; *p; p++) { + if (*p == ' ') + *p = '_'; + } + g_autofree char *cat_path = g_build_filename(g_get_user_data_dir(), "moemoji", + "kaomoji", dir_name, NULL); + g_mkdir_with_parents(cat_path, 0755); + + reload_categories(self); + navigate_to(self, "main", FALSE); +} + +static void on_entry_category_picked(GtkButton *button, gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + const char *dir = g_object_get_data(G_OBJECT(button), "cat-dir"); + g_free(self->selected_category_dir); + self->selected_category_dir = g_strdup(dir); + GtkTextBuffer *buf = + gtk_text_view_get_buffer(GTK_TEXT_VIEW(self->entry_text_view)); + gtk_text_buffer_set_text(buf, "", -1); + navigate_to(self, "add-entry", TRUE); +} + +static void build_pick_category_page(MoeMojiWindow *self, + GtkWidget *pick_flow) { + GtkWidget *child; + while ((child = gtk_widget_get_first_child(pick_flow)) != NULL) + gtk_flow_box_remove(GTK_FLOW_BOX(pick_flow), child); + + GPtrArray *entries = collect_all_categories(); + + for (guint i = 0; i < entries->len; i++) { + char **pair = g_ptr_array_index(entries, i); + g_autofree char *display = make_display_name(pair[1]); + g_autofree char *full_path = g_build_filename(pair[0], pair[1], NULL); + GtkWidget *btn = gtk_button_new_with_label(display); + gtk_widget_add_css_class(btn, "category-chip"); + gtk_widget_add_css_class(btn, "flat"); + g_object_set_data_full(G_OBJECT(btn), "cat-dir", g_strdup(full_path), + g_free); + g_signal_connect(btn, "clicked", G_CALLBACK(on_entry_category_picked), + self); + gtk_flow_box_insert(GTK_FLOW_BOX(pick_flow), btn, -1); + } + free_category_entries(entries); +} + +static void on_entry_save_clicked(G_GNUC_UNUSED GtkButton *button, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + if (!self->selected_category_dir) + return; + + GtkTextBuffer *buf = + gtk_text_view_get_buffer(GTK_TEXT_VIEW(self->entry_text_view)); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buf, &start, &end); + g_autofree char *text = gtk_text_buffer_get_text(buf, &start, &end, FALSE); + g_strchomp(text); + if (text[0] == '\0') + return; + int n = 1; + while (TRUE) { + g_autofree char *fname = g_strdup_printf("%d.txt", n); + g_autofree char *fpath = + g_build_filename(self->selected_category_dir, fname, NULL); + if (!g_file_test(fpath, G_FILE_TEST_EXISTS)) { + g_autofree char *with_newline = g_strdup_printf("%s\n", text); + g_file_set_contents(fpath, with_newline, -1, NULL); + break; + } + n++; + } + reload_categories(self); + navigate_to(self, "main", FALSE); +} + +static void reload_categories(MoeMojiWindow *self) { + GtkWidget *child; + while ((child = gtk_widget_get_first_child(GTK_WIDGET(self->content_box))) != + NULL) + gtk_box_remove(self->content_box, child); + g_ptr_array_set_size(self->category_widgets, 0); + self->active_chip_index = -1; + self->bottom_spacer = NULL; + + if (self->category_bar) { + gtk_box_remove(self->outer_box, GTK_WIDGET(self->category_bar)); + self->category_bar = NULL; + } + GPtrArray *entries = collect_all_categories(); + + if (entries->len == 0) { + GtkWidget *label = gtk_label_new("No kaomoji data found."); + gtk_box_append(self->content_box, label); + free_category_entries(entries); + return; + } + for (guint i = 0; i < entries->len; i++) { + char **pair = g_ptr_array_index(entries, i); + load_category(self, pair[0], pair[1]); + } + free_category_entries(entries); self->bottom_spacer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_box_append(self->content_box, self->bottom_spacer); - g_signal_connect(self->search_entry, "search-changed", - G_CALLBACK(on_search_changed), self); if (self->category_widgets->len > 0) { GtkWidget *flow = gtk_flow_box_new(); gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(flow), FALSE); @@ -368,14 +605,697 @@ static void moemoji_window_init(MoeMojiWindow *self) { gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(cat_scroll), flow); gtk_box_append(self->category_bar, cat_scroll); gtk_box_append(self->outer_box, GTK_WIDGET(self->category_bar)); - GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment( GTK_SCROLLED_WINDOW(self->kaomoji_scroll)); - g_signal_connect(vadj, "value-changed", G_CALLBACK(on_scroll_changed), - self); - g_signal_connect(vadj, "notify::page-size", - G_CALLBACK(on_page_size_changed), self); - g_signal_connect(self->kaomoji_scroll, "map", - G_CALLBACK(on_kaomoji_scroll_map), self); + if (self->scroll_handler_id != 0) + g_signal_handler_disconnect(vadj, self->scroll_handler_id); + if (self->page_size_handler_id != 0) + g_signal_handler_disconnect(vadj, self->page_size_handler_id); + self->scroll_handler_id = g_signal_connect( + vadj, "value-changed", G_CALLBACK(on_scroll_changed), self); + self->page_size_handler_id = g_signal_connect( + vadj, "notify::page-size", G_CALLBACK(on_page_size_changed), self); } } + +static void on_new_entry_clicked(G_GNUC_UNUSED GtkButton *button, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + GtkWidget *pick_page = + gtk_stack_get_child_by_name(self->view_stack, "pick-category"); + GtkWidget *pick_flow = g_object_get_data(G_OBJECT(pick_page), "pick-flow"); + build_pick_category_page(self, pick_flow); + navigate_to(self, "pick-category", TRUE); +} + +static GtkWidget *build_add_choice_page(MoeMojiWindow *self) { + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16); + gtk_widget_add_css_class(box, "add-form"); + gtk_widget_set_halign(box, GTK_ALIGN_CENTER); + gtk_widget_set_valign(box, GTK_ALIGN_CENTER); + gtk_widget_set_vexpand(box, TRUE); + + GtkWidget *cat_btn = gtk_button_new_with_label("New category!"); + gtk_widget_add_css_class(cat_btn, "add-choice-button"); + g_signal_connect(cat_btn, "clicked", G_CALLBACK(on_new_category_clicked), + self); + gtk_box_append(GTK_BOX(box), cat_btn); + GtkWidget *entry_btn = gtk_button_new_with_label("New emote!"); + gtk_widget_add_css_class(entry_btn, "add-choice-button"); + g_signal_connect(entry_btn, "clicked", G_CALLBACK(on_new_entry_clicked), + self); + gtk_box_append(GTK_BOX(box), entry_btn); + + return box; +} + +static GtkWidget *build_add_category_page(MoeMojiWindow *self) { + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_add_css_class(box, "add-form"); + gtk_widget_set_halign(box, GTK_ALIGN_CENTER); + gtk_widget_set_valign(box, GTK_ALIGN_CENTER); + gtk_widget_set_vexpand(box, TRUE); + gtk_widget_set_size_request(box, 300, -1); + GtkWidget *label = gtk_label_new("Name your category!"); + gtk_widget_add_css_class(label, "category-header"); + gtk_box_append(GTK_BOX(box), label); + self->category_name_entry = GTK_ENTRY(gtk_entry_new()); + gtk_entry_set_placeholder_text(self->category_name_entry, + "Something like 'cute'"); + gtk_box_append(GTK_BOX(box), GTK_WIDGET(self->category_name_entry)); + g_signal_connect(self->category_name_entry, "changed", + G_CALLBACK(on_category_name_changed), self); + + self->category_save_button = gtk_button_new_with_label("Save"); + gtk_widget_set_sensitive(self->category_save_button, FALSE); + gtk_widget_add_css_class(self->category_save_button, "suggested-action"); + g_signal_connect(self->category_save_button, "clicked", + G_CALLBACK(on_category_save_clicked), self); + gtk_box_append(GTK_BOX(box), self->category_save_button); + + return box; +} + +static GtkWidget *build_pick_category_page_widget(MoeMojiWindow *self) { + GtkWidget *wrapper = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_vexpand(wrapper, TRUE); + gtk_widget_set_halign(wrapper, GTK_ALIGN_CENTER); + gtk_widget_set_valign(wrapper, GTK_ALIGN_FILL); + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_add_css_class(box, "add-form"); + gtk_widget_set_vexpand(box, TRUE); + gtk_widget_set_size_request(box, 400, -1); + GtkWidget *clamp = adw_clamp_new(); + adw_clamp_set_maximum_size(ADW_CLAMP(clamp), 600); + adw_clamp_set_tightening_threshold(ADW_CLAMP(clamp), 400); + gtk_widget_set_vexpand(clamp, TRUE); + + GtkWidget *label = gtk_label_new("Pick a Category"); + gtk_widget_add_css_class(label, "category-header"); + gtk_box_append(GTK_BOX(box), label); + + GtkWidget *scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + + GtkWidget *pick_flow = gtk_flow_box_new(); + gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(pick_flow), FALSE); + gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(pick_flow), 20); + gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(pick_flow), GTK_SELECTION_NONE); + gtk_widget_add_css_class(pick_flow, "category-chips"); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), pick_flow); + gtk_box_append(GTK_BOX(box), scroll); + + adw_clamp_set_child(ADW_CLAMP(clamp), box); + gtk_box_append(GTK_BOX(wrapper), clamp); + g_object_set_data(G_OBJECT(wrapper), "pick-flow", pick_flow); + + (void)self; + return wrapper; +} + +static GtkWidget *build_add_entry_page(MoeMojiWindow *self) { + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_add_css_class(box, "add-form"); + gtk_widget_set_vexpand(box, TRUE); + + GtkWidget *label = gtk_label_new("New emote"); + gtk_widget_add_css_class(label, "category-header"); + gtk_box_append(GTK_BOX(box), label); + + GtkWidget *scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_widget_add_css_class(scroll, "entry-editor-frame"); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_size_request(scroll, -1, 100); + + self->entry_text_view = gtk_text_view_new(); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(self->entry_text_view), + GTK_WRAP_CHAR); + gtk_text_view_set_left_margin(GTK_TEXT_VIEW(self->entry_text_view), 8); + gtk_text_view_set_right_margin(GTK_TEXT_VIEW(self->entry_text_view), 8); + gtk_text_view_set_top_margin(GTK_TEXT_VIEW(self->entry_text_view), 8); + gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(self->entry_text_view), 8); + gtk_widget_add_css_class(self->entry_text_view, "entry-text-view"); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), + self->entry_text_view); + gtk_box_append(GTK_BOX(box), scroll); + + GtkWidget *save_btn = gtk_button_new_with_label("Save"); + gtk_widget_add_css_class(save_btn, "suggested-action"); + g_signal_connect(save_btn, "clicked", G_CALLBACK(on_entry_save_clicked), + self); + gtk_box_append(GTK_BOX(box), save_btn); + + return box; +} + +static void remove_dir_recursive(const char *path) { + GDir *dir = g_dir_open(path, 0, NULL); + if (!dir) + return; + const char *name; + while ((name = g_dir_read_name(dir)) != NULL) { + g_autofree char *child = g_build_filename(path, name, NULL); + if (g_file_test(child, G_FILE_TEST_IS_DIR)) + remove_dir_recursive(child); + else + g_unlink(child); + } + g_dir_close(dir); + g_rmdir(path); +} + +static void copy_dir_recursive(const char *src, const char *dst) { + g_mkdir_with_parents(dst, 0755); + GDir *dir = g_dir_open(src, 0, NULL); + if (!dir) + return; + const char *name; + while ((name = g_dir_read_name(dir)) != NULL) { + g_autofree char *s = g_build_filename(src, name, NULL); + g_autofree char *d = g_build_filename(dst, name, NULL); + if (g_file_test(s, G_FILE_TEST_IS_DIR)) { + copy_dir_recursive(s, d); + } else { + char *contents = NULL; + gsize len = 0; + if (g_file_get_contents(s, &contents, &len, NULL)) { + g_file_set_contents(d, contents, len, NULL); + g_free(contents); + } + } + } + g_dir_close(dir); +} + +static void populate_manage_page(MoeMojiWindow *self); + +static void on_manage_delete_emote_response(AdwAlertDialog *dialog, + GAsyncResult *res, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + const char *response = adw_alert_dialog_choose_finish(dialog, res); + if (g_strcmp0(response, "yes") == 0) { + const char *filepath = g_object_get_data(G_OBJECT(dialog), "emote-path"); + g_unlink(filepath); + reload_categories(self); + populate_manage_page(self); + } +} + +static void on_manage_delete_emote(GtkButton *button, gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + const char *filepath = g_object_get_data(G_OBJECT(button), "emote-path"); + const char *emote_text = g_object_get_data(G_OBJECT(button), "emote-text"); + + g_autofree char *body = NULL; + if (g_utf8_strlen(emote_text, -1) > 40) { + gchar *end = g_utf8_offset_to_pointer(emote_text, 40); + g_autofree char *prefix = g_strndup(emote_text, end - emote_text); + body = g_strdup_printf("%s…", prefix); + } else { + body = g_strdup(emote_text); + } + AdwAlertDialog *confirm = + ADW_ALERT_DIALOG(adw_alert_dialog_new("Delete emote?", body)); + adw_alert_dialog_add_responses(confirm, "no", "No", "yes", "Yes", NULL); + adw_alert_dialog_set_response_appearance(confirm, "yes", + ADW_RESPONSE_DESTRUCTIVE); + adw_alert_dialog_set_default_response(confirm, "no"); + adw_alert_dialog_set_close_response(confirm, "no"); + g_object_set_data_full(G_OBJECT(confirm), "emote-path", g_strdup(filepath), + g_free); + + adw_alert_dialog_choose(confirm, GTK_WIDGET(self), NULL, + (GAsyncReadyCallback)on_manage_delete_emote_response, + self); +} + +static void on_manage_delete_category_response(AdwAlertDialog *dialog, + GAsyncResult *res, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + const char *response = adw_alert_dialog_choose_finish(dialog, res); + if (g_strcmp0(response, "yes") == 0) { + const char *cat_path = g_object_get_data(G_OBJECT(dialog), "cat-path"); + remove_dir_recursive(cat_path); + reload_categories(self); + populate_manage_page(self); + } +} + +static void on_manage_delete_category(GtkButton *button, gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + const char *cat_path = g_object_get_data(G_OBJECT(button), "cat-path"); + const char *cat_name = g_object_get_data(G_OBJECT(button), "cat-name"); + + g_autofree char *heading = g_strdup_printf("Delete \"%s\"?", cat_name); + AdwAlertDialog *confirm = ADW_ALERT_DIALOG(adw_alert_dialog_new( + heading, "Are you sure? This will erase all emotes in this category.")); + adw_alert_dialog_add_responses(confirm, "no", "No", "yes", "Yes", NULL); + adw_alert_dialog_set_response_appearance(confirm, "yes", + ADW_RESPONSE_DESTRUCTIVE); + adw_alert_dialog_set_default_response(confirm, "no"); + adw_alert_dialog_set_close_response(confirm, "no"); + g_object_set_data_full(G_OBJECT(confirm), "cat-path", g_strdup(cat_path), + g_free); + + adw_alert_dialog_choose( + confirm, GTK_WIDGET(self), NULL, + (GAsyncReadyCallback)on_manage_delete_category_response, self); +} + +static void populate_manage_page(MoeMojiWindow *self) { + GtkWidget *manage_page = + gtk_stack_get_child_by_name(self->view_stack, "manage"); + GtkWidget *content = + g_object_get_data(G_OBJECT(manage_page), "manage-content"); + + GtkWidget *child; + while ((child = gtk_widget_get_first_child(content)) != NULL) + gtk_box_remove(GTK_BOX(content), child); + + GPtrArray *entries = collect_all_categories(); + + if (entries->len == 0) { + GtkWidget *label = gtk_label_new("No categories to manage."); + gtk_box_append(GTK_BOX(content), label); + } + + for (guint i = 0; i < entries->len; i++) { + char **pair = g_ptr_array_index(entries, i); + g_autofree char *cat_path = g_build_filename(pair[0], pair[1], NULL); + g_autofree char *display_name = make_display_name(pair[1]); + + GtkWidget *cat_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_margin_top(cat_hbox, 8); + + GtkWidget *cat_label = gtk_label_new(display_name); + gtk_widget_add_css_class(cat_label, "category-header"); + gtk_label_set_xalign(GTK_LABEL(cat_label), 0.0); + gtk_widget_set_hexpand(cat_label, TRUE); + gtk_box_append(GTK_BOX(cat_hbox), cat_label); + + GtkWidget *cat_del_btn = + gtk_button_new_from_icon_name("window-close-symbolic"); + gtk_widget_add_css_class(cat_del_btn, "manage-cat-delete"); + gtk_widget_add_css_class(cat_del_btn, "circular"); + gtk_widget_set_valign(cat_del_btn, GTK_ALIGN_CENTER); + g_object_set_data_full(G_OBJECT(cat_del_btn), "cat-path", + g_strdup(cat_path), g_free); + g_object_set_data_full(G_OBJECT(cat_del_btn), "cat-name", + g_strdup(display_name), g_free); + g_signal_connect(cat_del_btn, "clicked", + G_CALLBACK(on_manage_delete_category), self); + gtk_box_append(GTK_BOX(cat_hbox), cat_del_btn); + + gtk_box_append(GTK_BOX(content), cat_hbox); + + GtkWidget *flow = gtk_flow_box_new(); + gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(flow), FALSE); + gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(flow), 2); + gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(flow), 10); + gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(flow), GTK_SELECTION_NONE); + + GDir *dir = g_dir_open(cat_path, 0, NULL); + if (dir) { + GPtrArray *files = g_ptr_array_new_with_free_func(g_free); + const char *filename; + while ((filename = g_dir_read_name(dir)) != NULL) { + if (g_str_has_suffix(filename, ".txt")) + g_ptr_array_add(files, g_build_filename(cat_path, filename, NULL)); + } + g_ptr_array_sort(files, compare_files_by_mtime); + + for (guint j = 0; j < files->len; j++) { + const char *filepath = g_ptr_array_index(files, j); + char *contents = NULL; + if (g_file_get_contents(filepath, &contents, NULL, NULL)) { + g_strchomp(contents); + if (contents[0] != '\0') { + GtkWidget *emote_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_widget_add_css_class(emote_box, "manage-emote"); + gtk_widget_set_halign(emote_box, GTK_ALIGN_CENTER); + + const char *display_text = contents; + g_autofree char *first_line = NULL; + if (strchr(contents, '\n')) { + const char *nl = strchr(contents, '\n'); + first_line = g_strndup(contents, nl - contents); + display_text = first_line; + } + + GtkWidget *emote_label = gtk_label_new(display_text); + gtk_label_set_ellipsize(GTK_LABEL(emote_label), + PANGO_ELLIPSIZE_END); + gtk_label_set_max_width_chars(GTK_LABEL(emote_label), 20); + gtk_box_append(GTK_BOX(emote_box), emote_label); + + GtkWidget *del_btn = + gtk_button_new_from_icon_name("window-close-symbolic"); + gtk_widget_add_css_class(del_btn, "manage-delete"); + gtk_widget_add_css_class(del_btn, "circular"); + gtk_widget_set_halign(del_btn, GTK_ALIGN_CENTER); + g_object_set_data_full(G_OBJECT(del_btn), "emote-path", + g_strdup(filepath), g_free); + g_object_set_data_full(G_OBJECT(del_btn), "emote-text", + g_strdup(display_text), g_free); + g_signal_connect(del_btn, "clicked", + G_CALLBACK(on_manage_delete_emote), self); + gtk_box_append(GTK_BOX(emote_box), del_btn); + + gtk_flow_box_insert(GTK_FLOW_BOX(flow), emote_box, -1); + } + g_free(contents); + } + } + g_ptr_array_free(files, TRUE); + g_dir_close(dir); + } + + gtk_box_append(GTK_BOX(content), flow); + } + free_category_entries(entries); +} + +static void on_manage_search_changed(GtkSearchEntry *entry, + gpointer user_data) { + GtkWidget *manage_page = GTK_WIDGET(user_data); + const char *query = gtk_editable_get_text(GTK_EDITABLE(entry)); + if (query == NULL || query[0] == '\0') + return; + + GtkWidget *content = + g_object_get_data(G_OBJECT(manage_page), "manage-content"); + GtkWidget *scroll = g_object_get_data(G_OBJECT(manage_page), "manage-scroll"); + + g_autofree char *query_lower = g_utf8_strdown(query, -1); + + for (GtkWidget *child = gtk_widget_get_first_child(content); child != NULL; + child = gtk_widget_get_next_sibling(child)) { + GtkWidget *first = gtk_widget_get_first_child(child); + if (first == NULL || !GTK_IS_LABEL(first)) + continue; + const char *label_text = gtk_label_get_text(GTK_LABEL(first)); + g_autofree char *name_lower = g_utf8_strdown(label_text, -1); + if (strstr(name_lower, query_lower) != NULL) { + graphene_point_t p; + if (gtk_widget_compute_point(child, content, &GRAPHENE_POINT_INIT(0, 0), + &p)) { + GtkAdjustment *vadj = + gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll)); + gtk_adjustment_set_value(vadj, p.y); + } + return; + } + } +} + +static GtkWidget *build_manage_page(void) { + GtkWidget *wrapper = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_vexpand(wrapper, TRUE); + + GtkWidget *clamp = adw_clamp_new(); + adw_clamp_set_maximum_size(ADW_CLAMP(clamp), 600); + adw_clamp_set_tightening_threshold(ADW_CLAMP(clamp), 400); + gtk_widget_set_vexpand(clamp, TRUE); + + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_add_css_class(box, "add-form"); + gtk_widget_set_vexpand(box, TRUE); + + GtkWidget *search = gtk_search_entry_new(); + gtk_widget_set_margin_start(search, 6); + gtk_widget_set_margin_end(search, 6); + gtk_widget_set_margin_top(search, 6); + g_object_set(search, "placeholder-text", "Jump to category...", NULL); + gtk_box_append(GTK_BOX(box), search); + + GtkWidget *scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + + GtkWidget *content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_widget_set_margin_start(content, 6); + gtk_widget_set_margin_end(content, 6); + gtk_widget_set_margin_top(content, 6); + gtk_widget_set_margin_bottom(content, 6); + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), content); + gtk_box_append(GTK_BOX(box), scroll); + adw_clamp_set_child(ADW_CLAMP(clamp), box); + gtk_box_append(GTK_BOX(wrapper), clamp); + + g_object_set_data(G_OBJECT(wrapper), "manage-content", content); + g_object_set_data(G_OBJECT(wrapper), "manage-search", search); + g_object_set_data(G_OBJECT(wrapper), "manage-scroll", scroll); + + g_signal_connect(search, "search-changed", + G_CALLBACK(on_manage_search_changed), wrapper); + + return wrapper; +} + +static void on_manage_activated(G_GNUC_UNUSED GSimpleAction *action, + G_GNUC_UNUSED GVariant *parameter, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + populate_manage_page(self); + GtkWidget *manage_page = + gtk_stack_get_child_by_name(self->view_stack, "manage"); + GtkWidget *search = g_object_get_data(G_OBJECT(manage_page), "manage-search"); + gtk_editable_set_text(GTK_EDITABLE(search), ""); + navigate_to(self, "manage", TRUE); +} + +static void on_export_save_ready(GObject *source, GAsyncResult *res, + G_GNUC_UNUSED gpointer user_data) { + GtkFileDialog *dialog = GTK_FILE_DIALOG(source); + GFile *file = gtk_file_dialog_save_finish(dialog, res, NULL); + if (!file) + return; + + g_autofree char *path = g_file_get_path(file); + g_object_unref(file); + + g_autofree char *tmpdir = g_dir_make_tmp("moemoji-export-XXXXXX", NULL); + if (!tmpdir) + return; + g_autofree char *user_dir = + g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL); + if (g_file_test(user_dir, G_FILE_TEST_IS_DIR)) { + GDir *dir = g_dir_open(user_dir, 0, NULL); + if (dir) { + const char *name; + while ((name = g_dir_read_name(dir)) != NULL) { + g_autofree char *src = g_build_filename(user_dir, name, NULL); + if (g_file_test(src, G_FILE_TEST_IS_DIR)) { + g_autofree char *dst = g_build_filename(tmpdir, name, NULL); + copy_dir_recursive(src, dst); + } + } + g_dir_close(dir); + } + } + char *kaomoji_dir = find_kaomoji_dir(); + if (kaomoji_dir) { + GDir *dir = g_dir_open(kaomoji_dir, 0, NULL); + if (dir) { + const char *name; + while ((name = g_dir_read_name(dir)) != NULL) { + g_autofree char *dst = g_build_filename(tmpdir, name, NULL); + if (g_file_test(dst, G_FILE_TEST_IS_DIR)) + continue; + g_autofree char *src = g_build_filename(kaomoji_dir, name, NULL); + if (g_file_test(src, G_FILE_TEST_IS_DIR)) + copy_dir_recursive(src, dst); + } + g_dir_close(dir); + } + g_free(kaomoji_dir); + } + GSubprocess *tar = g_subprocess_new(G_SUBPROCESS_FLAGS_NONE, NULL, "tar", + "czf", path, "-C", tmpdir, ".", NULL); + if (tar) { + g_subprocess_wait(tar, NULL, NULL); + g_object_unref(tar); + } + + remove_dir_recursive(tmpdir); +} + +static void on_export_activated(G_GNUC_UNUSED GSimpleAction *action, + G_GNUC_UNUSED GVariant *parameter, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + GtkFileDialog *dialog = gtk_file_dialog_new(); + gtk_file_dialog_set_title(dialog, "Export Kaomojis"); + gtk_file_dialog_set_initial_name(dialog, "moemoji-export.tar.gz"); + GListStore *filters = g_list_store_new(GTK_TYPE_FILE_FILTER); + GtkFileFilter *filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, "Tar archives (*.tar.gz)"); + gtk_file_filter_add_pattern(filter, "*.tar.gz"); + g_list_store_append(filters, filter); + g_object_unref(filter); + gtk_file_dialog_set_filters(dialog, G_LIST_MODEL(filters)); + g_object_unref(filters); + gtk_file_dialog_save(dialog, GTK_WINDOW(self), NULL, on_export_save_ready, + self); + g_object_unref(dialog); +} + +static void clear_user_categories(void) { + g_autofree char *user_dir = + g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL); + GDir *dir = g_dir_open(user_dir, 0, NULL); + if (!dir) + return; + const char *name; + while ((name = g_dir_read_name(dir)) != NULL) { + g_autofree char *child = g_build_filename(user_dir, name, NULL); + if (g_file_test(child, G_FILE_TEST_IS_DIR)) + remove_dir_recursive(child); + } + g_dir_close(dir); +} + +static void do_import(MoeMojiWindow *self, const char *fpath) { + g_autofree char *user_dir = + g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL); + clear_user_categories(); + g_mkdir_with_parents(user_dir, 0755); + GSubprocess *tar = g_subprocess_new(G_SUBPROCESS_FLAGS_NONE, NULL, "tar", + "xzf", fpath, "-C", user_dir, NULL); + if (tar) { + g_subprocess_wait(tar, NULL, NULL); + g_object_unref(tar); + } + reload_categories(self); +} + +static void on_import_confirm_response(AdwAlertDialog *dialog, + GAsyncResult *res, gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + const char *response = adw_alert_dialog_choose_finish(dialog, res); + if (g_strcmp0(response, "import") == 0) { + const char *path = g_object_get_data(G_OBJECT(dialog), "import-path"); + do_import(self, path); + } +} + +static void on_import_open_ready(GObject *source, GAsyncResult *res, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + GtkFileDialog *dialog = GTK_FILE_DIALOG(source); + GFile *file = gtk_file_dialog_open_finish(dialog, res, NULL); + if (!file) + return; + char *fpath = g_file_get_path(file); + g_object_unref(file); + AdwAlertDialog *confirm = ADW_ALERT_DIALOG(adw_alert_dialog_new( + "Import Kaomojis?", + "Importing will erase your previous kaomojis. Proceed?")); + adw_alert_dialog_add_responses(confirm, "cancel", "No", "import", "Yes", + NULL); + adw_alert_dialog_set_response_appearance(confirm, "import", + ADW_RESPONSE_DESTRUCTIVE); + adw_alert_dialog_set_default_response(confirm, "cancel"); + adw_alert_dialog_set_close_response(confirm, "cancel"); + g_object_set_data_full(G_OBJECT(confirm), "import-path", fpath, g_free); + + adw_alert_dialog_choose(confirm, GTK_WIDGET(self), NULL, + (GAsyncReadyCallback)on_import_confirm_response, + self); +} + +static void on_import_activated(G_GNUC_UNUSED GSimpleAction *action, + G_GNUC_UNUSED GVariant *parameter, + gpointer user_data) { + MoeMojiWindow *self = MOEMOJI_WINDOW(user_data); + GtkFileDialog *dialog = gtk_file_dialog_new(); + gtk_file_dialog_set_title(dialog, "Import Kaomojis"); + GListStore *filters = g_list_store_new(GTK_TYPE_FILE_FILTER); + GtkFileFilter *filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, "Tar archives (*.tar.gz)"); + gtk_file_filter_add_pattern(filter, "*.tar.gz"); + g_list_store_append(filters, filter); + g_object_unref(filter); + gtk_file_dialog_set_filters(dialog, G_LIST_MODEL(filters)); + g_object_unref(filters); + gtk_file_dialog_open(dialog, GTK_WINDOW(self), NULL, on_import_open_ready, + self); + g_object_unref(dialog); +} + +static void moemoji_window_class_init(MoeMojiWindowClass *klass) { + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + gtk_widget_class_set_template_from_resource( + widget_class, "/jp/angeltech/MoeMoji/moemoji-window.ui"); + gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, outer_box); + gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, + content_box); + gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, + search_entry); + gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, header_bar); + gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, + kaomoji_scroll); + gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, view_stack); + gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, add_button); + gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, + back_button); + gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, + menu_button); +} + +static void moemoji_window_init(MoeMojiWindow *self) { + gtk_widget_init_template(GTK_WIDGET(self)); + gtk_widget_add_css_class(GTK_WIDGET(self), "wallpaper-bg"); + gtk_widget_add_css_class(GTK_WIDGET(self->header_bar), "wallpaper-bg"); + gtk_widget_add_css_class(GTK_WIDGET(self->content_box), "content-area"); + self->category_widgets = + g_ptr_array_new_with_free_func(category_widgets_free); + self->active_chip_index = -1; + self->selected_category_dir = NULL; + + GSimpleAction *export_action = g_simple_action_new("export", NULL); + g_signal_connect(export_action, "activate", G_CALLBACK(on_export_activated), + self); + g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(export_action)); + g_object_unref(export_action); + + GSimpleAction *import_action = g_simple_action_new("import", NULL); + g_signal_connect(import_action, "activate", G_CALLBACK(on_import_activated), + self); + g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(import_action)); + g_object_unref(import_action); + + GSimpleAction *manage_action = g_simple_action_new("manage", NULL); + g_signal_connect(manage_action, "activate", G_CALLBACK(on_manage_activated), + self); + g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(manage_action)); + g_object_unref(manage_action); + + g_signal_connect(self->add_button, "clicked", G_CALLBACK(on_add_clicked), + self); + g_signal_connect(self->back_button, "clicked", G_CALLBACK(on_back_clicked), + self); + GtkWidget *choice_page = build_add_choice_page(self); + gtk_stack_add_named(self->view_stack, choice_page, "add-choice"); + GtkWidget *cat_page = build_add_category_page(self); + gtk_stack_add_named(self->view_stack, cat_page, "add-category"); + GtkWidget *pick_page = build_pick_category_page_widget(self); + gtk_stack_add_named(self->view_stack, pick_page, "pick-category"); + GtkWidget *entry_page = build_add_entry_page(self); + gtk_stack_add_named(self->view_stack, entry_page, "add-entry"); + GtkWidget *manage_page = build_manage_page(); + gtk_stack_add_named(self->view_stack, manage_page, "manage"); + reload_categories(self); + g_signal_connect(self->search_entry, "search-changed", + G_CALLBACK(on_search_changed), self); + g_signal_connect(self->kaomoji_scroll, "map", + G_CALLBACK(on_kaomoji_scroll_map), self); +} diff --git a/src/moemoji-window.h b/src/moemoji-window.h index de6e737..872466e 100644 --- a/src/moemoji-window.h +++ b/src/moemoji-window.h @@ -20,6 +20,16 @@ struct _MoeMojiWindow { GPtrArray *category_widgets; GtkWidget *bottom_spacer; int active_chip_index; + GtkStack *view_stack; + GtkWidget *add_button; + GtkWidget *back_button; + GtkWidget *menu_button; + GtkEntry *category_name_entry; + GtkWidget *entry_text_view; + GtkWidget *category_save_button; + char *selected_category_dir; + gulong scroll_handler_id; + gulong page_size_handler_id; }; G_BEGIN_DECLS diff --git a/src/moemoji-window.ui b/src/moemoji-window.ui index c4194ed..4628b3b 100644 --- a/src/moemoji-window.ui +++ b/src/moemoji-window.ui @@ -10,37 +10,84 @@ True + + + list-add-symbolic + + + + + go-previous-symbolic + False + + + + + open-menu-symbolic + primary_menu + + - - vertical + + slide-left + 200 - - Jump to category... - 6 - 6 - 6 - - - - - never - True - - + + main + + vertical - 6 - 6 - 6 - 6 - 4 + + + Jump to category... + 6 + 6 + 6 + + + + + never + True + + + vertical + 6 + 6 + 6 + 6 + 4 + + + + - + + + +
+ + Manage… + win.manage + +
+
+ + Export… + win.export + + + Import… + win.import + +
+
\ No newline at end of file diff --git a/src/style.css b/src/style.css index 51f3a38..1a68d8e 100644 --- a/src/style.css +++ b/src/style.css @@ -37,7 +37,7 @@ color: white; } -wallpaper-bg > * { +.wallpaper-bg > * { background-color: transparent; } @@ -59,3 +59,50 @@ wallpaper-bg > * { background-color: rgba(255, 255, 255, 0.25); border-radius: 8px; } + +.add-choice-button { + min-width: 200px; + min-height: 48px; + font-size: 16px; +} + +.add-form { + padding: 24px; + margin: 12px; +} + +.entry-editor-frame { + background-color: rgba(0, 0, 0, 0.5); + border-radius: 12px; + overflow: hidden; +} + +.entry-text-view, .entry-text-view text { + background-color: transparent; + color: white; + caret-color: white; +} + +.manage-emote { + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 8px; + padding: 6px 10px; +} + +.manage-delete { + min-width: 20px; + min-height: 20px; + padding: 2px; + font-size: 10px; + background-color: @destructive_bg_color; + color: @destructive_fg_color; +} + +.manage-cat-delete { + min-width: 24px; + min-height: 24px; + padding: 2px; + background-color: @destructive_bg_color; + color: @destructive_fg_color; +} + diff --git a/tests/test-moemoji.c b/tests/test-moemoji.c index 63888b1..0bf0bdf 100644 --- a/tests/test-moemoji.c +++ b/tests/test-moemoji.c @@ -1,294 +1,601 @@ +#include "moemoji-internal.h" +#include "moemoji-window.h" #include #include #include -#include "moemoji-internal.h" -#include "moemoji-window.h" -static void -test_display_name_underscores (void) -{ - char *name = make_display_name ("happy_faces"); - g_assert_cmpstr (name, ==, "Happy faces"); - g_free (name); +static void test_display_name_underscores(void) { + char *name = make_display_name("happy_faces"); + g_assert_cmpstr(name, ==, "Happy faces"); + g_free(name); } -static void -test_display_name_no_underscores (void) -{ - char *name = make_display_name ("animals"); - g_assert_cmpstr (name, ==, "Animals"); - g_free (name); +static void test_display_name_no_underscores(void) { + char *name = make_display_name("animals"); + g_assert_cmpstr(name, ==, "Animals"); + g_free(name); } -static void -test_display_name_already_upper (void) -{ - char *name = make_display_name ("Animals"); - g_assert_cmpstr (name, ==, "Animals"); - g_free (name); +static void test_display_name_already_upper(void) { + char *name = make_display_name("Animals"); + g_assert_cmpstr(name, ==, "Animals"); + g_free(name); } -static void -test_display_name_single_char (void) -{ - char *name = make_display_name ("x"); - g_assert_cmpstr (name, ==, "X"); - g_free (name); +static void test_display_name_single_char(void) { + char *name = make_display_name("x"); + g_assert_cmpstr(name, ==, "X"); + g_free(name); } -static void -test_display_name_empty (void) -{ - char *name = make_display_name (""); - g_assert_cmpstr (name, ==, ""); - g_free (name); +static void test_display_name_empty(void) { + char *name = make_display_name(""); + g_assert_cmpstr(name, ==, ""); + g_free(name); } -static void -test_find_kaomoji_with_env (void) -{ - char *dir = find_kaomoji_dir (); - g_assert_nonnull (dir); - g_assert_true (g_file_test (dir, G_FILE_TEST_IS_DIR)); - g_free (dir); +static void test_find_kaomoji_with_env(void) { + char *dir = find_kaomoji_dir(); + g_assert_nonnull(dir); + g_assert_true(g_file_test(dir, G_FILE_TEST_IS_DIR)); + g_free(dir); } static gboolean find_kaomoji_bogus_passed = FALSE; -static void -run_find_kaomoji_bogus (void) -{ - g_setenv ("MESON_SOURCE_ROOT", "/nonexistent", TRUE); - char *saved = g_get_current_dir (); - g_assert_true (g_chdir ("/tmp") == 0); +static void run_find_kaomoji_bogus(void) { + g_setenv("MESON_SOURCE_ROOT", "/nonexistent", TRUE); + char *saved = g_get_current_dir(); + g_assert_true(g_chdir("/tmp") == 0); - char *dir = find_kaomoji_dir (); - g_free (dir); + char *dir = find_kaomoji_dir(); + g_free(dir); - g_assert_true (g_chdir (saved) == 0); - g_free (saved); - g_setenv ("MESON_SOURCE_ROOT", SRCDIR, TRUE); - find_kaomoji_bogus_passed = TRUE; + g_assert_true(g_chdir(saved) == 0); + g_free(saved); + g_setenv("MESON_SOURCE_ROOT", SRCDIR, TRUE); + find_kaomoji_bogus_passed = TRUE; } -static void -test_find_kaomoji_bogus (void) -{ - g_assert_true (find_kaomoji_bogus_passed); +static void test_find_kaomoji_bogus(void) { + g_assert_true(find_kaomoji_bogus_passed); } -static void -test_sni_category (void) -{ - GVariant *v = sni_get_property (NULL, NULL, NULL, NULL, "Category", NULL, NULL); - g_assert_nonnull (v); - g_assert_cmpstr (g_variant_get_string (v, NULL), ==, "ApplicationStatus"); - g_variant_unref (v); +static void test_sni_category(void) { + GVariant *v = + sni_get_property(NULL, NULL, NULL, NULL, "Category", NULL, NULL); + g_assert_nonnull(v); + g_assert_cmpstr(g_variant_get_string(v, NULL), ==, "ApplicationStatus"); + g_variant_unref(v); } -static void -test_sni_id (void) -{ - GVariant *v = sni_get_property (NULL, NULL, NULL, NULL, "Id", NULL, NULL); - g_assert_nonnull (v); - g_assert_cmpstr (g_variant_get_string (v, NULL), ==, "moemoji"); - g_variant_unref (v); +static void test_sni_id(void) { + GVariant *v = sni_get_property(NULL, NULL, NULL, NULL, "Id", NULL, NULL); + g_assert_nonnull(v); + g_assert_cmpstr(g_variant_get_string(v, NULL), ==, "moemoji"); + g_variant_unref(v); } -static void -test_sni_item_is_menu (void) -{ - GVariant *v = sni_get_property (NULL, NULL, NULL, NULL, "ItemIsMenu", NULL, NULL); - g_assert_nonnull (v); - g_assert_false (g_variant_get_boolean (v)); - g_variant_unref (v); +static void test_sni_item_is_menu(void) { + GVariant *v = + sni_get_property(NULL, NULL, NULL, NULL, "ItemIsMenu", NULL, NULL); + g_assert_nonnull(v); + g_assert_false(g_variant_get_boolean(v)); + g_variant_unref(v); } -static void -test_sni_menu (void) -{ - GVariant *v = sni_get_property (NULL, NULL, NULL, NULL, "Menu", NULL, NULL); - g_assert_nonnull (v); - g_assert_cmpstr (g_variant_get_string (v, NULL), ==, "/MenuBar"); - g_variant_unref (v); +static void test_sni_menu(void) { + GVariant *v = sni_get_property(NULL, NULL, NULL, NULL, "Menu", NULL, NULL); + g_assert_nonnull(v); + g_assert_cmpstr(g_variant_get_string(v, NULL), ==, "/MenuBar"); + g_variant_unref(v); } -static void -test_sni_unknown (void) -{ - GVariant *v = sni_get_property (NULL, NULL, NULL, NULL, "Nonexistent", NULL, NULL); - g_assert_null (v); +static void test_sni_unknown(void) { + GVariant *v = + sni_get_property(NULL, NULL, NULL, NULL, "Nonexistent", NULL, NULL); + g_assert_null(v); } -static void -test_dbusmenu_version (void) -{ - GVariant *v = dbusmenu_get_property (NULL, NULL, NULL, NULL, "Version", NULL, NULL); - g_assert_nonnull (v); - g_assert_cmpuint (g_variant_get_uint32 (v), ==, 3); - g_variant_unref (v); +static void test_dbusmenu_version(void) { + GVariant *v = + dbusmenu_get_property(NULL, NULL, NULL, NULL, "Version", NULL, NULL); + g_assert_nonnull(v); + g_assert_cmpuint(g_variant_get_uint32(v), ==, 3); + g_variant_unref(v); } -static void -test_dbusmenu_status (void) -{ - GVariant *v = dbusmenu_get_property (NULL, NULL, NULL, NULL, "Status", NULL, NULL); - g_assert_nonnull (v); - g_assert_cmpstr (g_variant_get_string (v, NULL), ==, "normal"); - g_variant_unref (v); +static void test_dbusmenu_status(void) { + GVariant *v = + dbusmenu_get_property(NULL, NULL, NULL, NULL, "Status", NULL, NULL); + g_assert_nonnull(v); + g_assert_cmpstr(g_variant_get_string(v, NULL), ==, "normal"); + g_variant_unref(v); } -static void -test_dbusmenu_text_direction (void) -{ - GVariant *v = dbusmenu_get_property (NULL, NULL, NULL, NULL, "TextDirection", NULL, NULL); - g_assert_nonnull (v); - g_assert_cmpstr (g_variant_get_string (v, NULL), ==, "ltr"); - g_variant_unref (v); +static void test_dbusmenu_text_direction(void) { + GVariant *v = dbusmenu_get_property(NULL, NULL, NULL, NULL, "TextDirection", + NULL, NULL); + g_assert_nonnull(v); + g_assert_cmpstr(g_variant_get_string(v, NULL), ==, "ltr"); + g_variant_unref(v); } -static void -test_dbusmenu_unknown (void) -{ - GVariant *v = dbusmenu_get_property (NULL, NULL, NULL, NULL, "Bogus", NULL, NULL); - g_assert_null (v); +static void test_dbusmenu_unknown(void) { + GVariant *v = + dbusmenu_get_property(NULL, NULL, NULL, NULL, "Bogus", NULL, NULL); + g_assert_null(v); } -static void -test_category_widgets_chip_init (void) -{ - CategoryWidgets *cw = g_new0 (CategoryWidgets, 1); - g_assert_null (cw->chip); - g_assert_null (cw->header); - g_assert_null (cw->flow); - g_assert_null (cw->name); - g_free (cw); +static void test_category_widgets_chip_init(void) { + CategoryWidgets *cw = g_new0(CategoryWidgets, 1); + g_assert_null(cw->chip); + g_assert_null(cw->header); + g_assert_null(cw->flow); + g_assert_null(cw->name); + g_free(cw); } -G_GNUC_INTERNAL GResource *moemoji_get_resource (void); +G_GNUC_INTERNAL GResource *moemoji_get_resource(void); static GResource *test_res = NULL; static GtkWidget *test_win = NULL; static MoeMojiWindow *test_self = NULL; -static void -window_test_setup (void) -{ - if (!test_res) { - test_res = moemoji_get_resource (); - g_resources_register (test_res); +static void window_test_setup(void) { + if (!test_res) { + test_res = moemoji_get_resource(); + g_resources_register(test_res); + } + test_win = g_object_new(MOEMOJI_TYPE_WINDOW, NULL); + test_self = MOEMOJI_WINDOW(test_win); +} + +static void window_test_teardown(void) { + gtk_window_destroy(GTK_WINDOW(test_win)); + test_win = NULL; + test_self = NULL; +} + +static void test_window_chip_setup(void) { + window_test_setup(); + + g_assert_cmpuint(test_self->category_widgets->len, >, 0); + + for (guint i = 0; i < test_self->category_widgets->len; i++) { + CategoryWidgets *cw = g_ptr_array_index(test_self->category_widgets, i); + g_assert_nonnull(cw->chip); + } + + window_test_teardown(); +} + +static void test_window_chip_labels_match(void) { + window_test_setup(); + + for (guint i = 0; i < test_self->category_widgets->len; i++) { + CategoryWidgets *cw = g_ptr_array_index(test_self->category_widgets, i); + const char *chip_label = gtk_button_get_label(GTK_BUTTON(cw->chip)); + g_assert_cmpstr(chip_label, ==, cw->name); + } + + window_test_teardown(); +} + +static void test_window_initial_active_chip(void) { + window_test_setup(); + + g_assert_cmpint(test_self->active_chip_index, ==, -1); + + window_test_teardown(); +} + +static void test_window_all_categories_visible(void) { + window_test_setup(); + + for (guint i = 0; i < test_self->category_widgets->len; i++) { + CategoryWidgets *cw = g_ptr_array_index(test_self->category_widgets, i); + g_assert_true(gtk_widget_get_visible(cw->header)); + g_assert_true(gtk_widget_get_visible(cw->flow)); + } + + window_test_teardown(); +} + +static void test_window_bottom_spacer(void) { + window_test_setup(); + + g_assert_nonnull(test_self->bottom_spacer); + GtkWidget *last = + gtk_widget_get_last_child(GTK_WIDGET(test_self->content_box)); + g_assert_true(last == test_self->bottom_spacer); + + window_test_teardown(); +} + +static void test_window_view_stack_exists(void) { + window_test_setup(); + g_assert_nonnull(test_self->view_stack); + g_assert_true(GTK_IS_STACK(test_self->view_stack)); + const char *name = gtk_stack_get_visible_child_name(test_self->view_stack); + g_assert_cmpstr(name, ==, "main"); + + window_test_teardown(); +} + +static void test_window_add_button_navigates(void) { + window_test_setup(); + g_assert_nonnull(test_self->add_button); + g_assert_nonnull(test_self->back_button); + g_assert_true(gtk_widget_get_visible(test_self->add_button)); + g_assert_false(gtk_widget_get_visible(test_self->back_button)); + g_signal_emit_by_name(test_self->add_button, "clicked"); + const char *name = gtk_stack_get_visible_child_name(test_self->view_stack); + g_assert_cmpstr(name, ==, "add-choice"); + g_assert_false(gtk_widget_get_visible(test_self->add_button)); + g_assert_true(gtk_widget_get_visible(test_self->back_button)); + g_signal_emit_by_name(test_self->back_button, "clicked"); + name = gtk_stack_get_visible_child_name(test_self->view_stack); + g_assert_cmpstr(name, ==, "main"); + g_assert_true(gtk_widget_get_visible(test_self->add_button)); + g_assert_false(gtk_widget_get_visible(test_self->back_button)); + + window_test_teardown(); +} + +static void test_window_stack_pages_exist(void) { + window_test_setup(); + + g_assert_nonnull(gtk_stack_get_child_by_name(test_self->view_stack, "main")); + g_assert_nonnull( + gtk_stack_get_child_by_name(test_self->view_stack, "add-choice")); + g_assert_nonnull( + gtk_stack_get_child_by_name(test_self->view_stack, "add-category")); + g_assert_nonnull( + gtk_stack_get_child_by_name(test_self->view_stack, "pick-category")); + g_assert_nonnull( + gtk_stack_get_child_by_name(test_self->view_stack, "add-entry")); + g_assert_nonnull( + gtk_stack_get_child_by_name(test_self->view_stack, "manage")); + + window_test_teardown(); +} + +static void test_add_category_creates_dir(void) { + char *cat_dir = g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", + "ZZZ_test_category", NULL); + g_mkdir_with_parents(cat_dir, 0755); + window_test_setup(); + gboolean found = FALSE; + for (guint i = 0; i < test_self->category_widgets->len; i++) { + CategoryWidgets *cw = g_ptr_array_index(test_self->category_widgets, i); + if (g_strcmp0(cw->name, "ZZZ test category") == 0) { + found = TRUE; + break; } - test_win = g_object_new (MOEMOJI_TYPE_WINDOW, NULL); - test_self = MOEMOJI_WINDOW (test_win); + } + g_assert_true(found); + window_test_teardown(); + g_rmdir(cat_dir); + char *kaomoji_parent = + g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL); + g_rmdir(kaomoji_parent); + g_free(kaomoji_parent); + char *moemoji_parent = + g_build_filename(g_get_user_data_dir(), "moemoji", NULL); + g_rmdir(moemoji_parent); + g_free(moemoji_parent); + g_free(cat_dir); } -static void -window_test_teardown (void) -{ - gtk_window_destroy (GTK_WINDOW (test_win)); - test_win = NULL; - test_self = NULL; -} - -static void -test_window_chip_setup (void) -{ - window_test_setup (); - - g_assert_cmpuint (test_self->category_widgets->len, >, 0); - - for (guint i = 0; i < test_self->category_widgets->len; i++) { - CategoryWidgets *cw = g_ptr_array_index (test_self->category_widgets, i); - g_assert_nonnull (cw->chip); +static void test_add_entry_creates_file(void) { + char *cat_dir = g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", + "ZZZ_test_entry_cat", NULL); + g_mkdir_with_parents(cat_dir, 0755); + char *fpath = g_build_filename(cat_dir, "1.txt", NULL); + g_file_set_contents(fpath, "(╯°□°)╯︵ ┻━┻\n", -1, NULL); + window_test_setup(); + gboolean found = FALSE; + for (guint i = 0; i < test_self->category_widgets->len; i++) { + CategoryWidgets *cw = g_ptr_array_index(test_self->category_widgets, i); + if (g_strcmp0(cw->name, "ZZZ test entry cat") == 0) { + GtkWidget *first = gtk_widget_get_first_child(cw->flow); + g_assert_nonnull(first); + found = TRUE; + break; } + } + g_assert_true(found); - window_test_teardown (); + g_free(test_self->selected_category_dir); + test_self->selected_category_dir = g_strdup(cat_dir); + GtkTextBuffer *buf = + gtk_text_view_get_buffer(GTK_TEXT_VIEW(test_self->entry_text_view)); + gtk_text_buffer_set_text(buf, "( ˘ω˘ )", -1); + + GtkWidget *entry_page = + gtk_stack_get_child_by_name(test_self->view_stack, "add-entry"); + GtkWidget *save_btn = gtk_widget_get_last_child(entry_page); + g_assert_nonnull(save_btn); + g_signal_emit_by_name(save_btn, "clicked"); + char *fpath2 = g_build_filename(cat_dir, "2.txt", NULL); + g_assert_true(g_file_test(fpath2, G_FILE_TEST_EXISTS)); + char *contents = NULL; + g_file_get_contents(fpath2, &contents, NULL, NULL); + g_assert_nonnull(contents); + g_strchomp(contents); + g_assert_cmpstr(contents, ==, "( ˘ω˘ )"); + g_free(contents); + const char *name = gtk_stack_get_visible_child_name(test_self->view_stack); + g_assert_cmpstr(name, ==, "main"); + + window_test_teardown(); + + g_unlink(fpath); + g_unlink(fpath2); + g_rmdir(cat_dir); + char *kaomoji_parent = + g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL); + g_rmdir(kaomoji_parent); + g_free(kaomoji_parent); + char *moemoji_parent = + g_build_filename(g_get_user_data_dir(), "moemoji", NULL); + g_rmdir(moemoji_parent); + g_free(moemoji_parent); + g_free(cat_dir); + g_free(fpath); + g_free(fpath2); } -static void -test_window_chip_labels_match (void) -{ - window_test_setup (); +static void test_export_creates_archive(void) { + char *user_dir = + g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL); + char *cat_dir = g_build_filename(user_dir, "ZZZ_export_test", NULL); + g_mkdir_with_parents(cat_dir, 0755); + char *fpath = g_build_filename(cat_dir, "1.txt", NULL); + g_file_set_contents(fpath, "(ノ◕ヮ◕)ノ*:・゚✧\n", -1, NULL); + char *tmpdir = g_dir_make_tmp("moemoji-test-XXXXXX", NULL); + g_assert_nonnull(tmpdir); + char *archive_path = g_build_filename(tmpdir, "test-export.tar.gz", NULL); + GSubprocess *tar = + g_subprocess_new(G_SUBPROCESS_FLAGS_NONE, NULL, "tar", "czf", + archive_path, "-C", user_dir, ".", NULL); + g_assert_nonnull(tar); + g_assert_true(g_subprocess_wait_check(tar, NULL, NULL)); + g_object_unref(tar); + g_assert_true(g_file_test(archive_path, G_FILE_TEST_EXISTS)); + char *extract_dir = g_build_filename(tmpdir, "extracted", NULL); + g_mkdir_with_parents(extract_dir, 0755); - for (guint i = 0; i < test_self->category_widgets->len; i++) { - CategoryWidgets *cw = g_ptr_array_index (test_self->category_widgets, i); - const char *chip_label = gtk_button_get_label (GTK_BUTTON (cw->chip)); - g_assert_cmpstr (chip_label, ==, cw->name); + GSubprocess *untar = + g_subprocess_new(G_SUBPROCESS_FLAGS_NONE, NULL, "tar", "xzf", + archive_path, "-C", extract_dir, NULL); + g_assert_nonnull(untar); + g_assert_true(g_subprocess_wait_check(untar, NULL, NULL)); + g_object_unref(untar); + + char *extracted_file = + g_build_filename(extract_dir, "ZZZ_export_test", "1.txt", NULL); + g_assert_true(g_file_test(extracted_file, G_FILE_TEST_EXISTS)); + char *contents = NULL; + g_file_get_contents(extracted_file, &contents, NULL, NULL); + g_assert_nonnull(contents); + g_strchomp(contents); + g_assert_cmpstr(contents, ==, "(ノ◕ヮ◕)ノ*:・゚✧"); + g_free(contents); + + g_unlink(extracted_file); + g_free(extracted_file); + char *extracted_cat = g_build_filename(extract_dir, "ZZZ_export_test", NULL); + g_rmdir(extracted_cat); + g_free(extracted_cat); + g_rmdir(extract_dir); + g_free(extract_dir); + g_unlink(archive_path); + g_free(archive_path); + g_rmdir(tmpdir); + g_free(tmpdir); + + g_unlink(fpath); + g_free(fpath); + g_rmdir(cat_dir); + g_free(cat_dir); + char *kaomoji_parent = + g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL); + g_rmdir(kaomoji_parent); + g_free(kaomoji_parent); + char *moemoji_parent = + g_build_filename(g_get_user_data_dir(), "moemoji", NULL); + g_rmdir(moemoji_parent); + g_free(moemoji_parent); + g_free(user_dir); +} + +static void test_import_extracts_and_loads(void) { + char *src_dir = g_dir_make_tmp("moemoji-import-src-XXXXXX", NULL); + g_assert_nonnull(src_dir); + char *cat_dir = g_build_filename(src_dir, "ZZZ_import_test", NULL); + g_mkdir_with_parents(cat_dir, 0755); + + char *fpath = g_build_filename(cat_dir, "1.txt", NULL); + g_file_set_contents(fpath, "ヽ(>∀<☆)ノ\n", -1, NULL); + + char *archive_path = g_build_filename(src_dir, "import-test.tar.gz", NULL); + GSubprocess *tar = + g_subprocess_new(G_SUBPROCESS_FLAGS_NONE, NULL, "tar", "czf", + archive_path, "-C", src_dir, "ZZZ_import_test", NULL); + g_assert_nonnull(tar); + g_assert_true(g_subprocess_wait_check(tar, NULL, NULL)); + g_object_unref(tar); + + char *user_dir = + g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL); + g_mkdir_with_parents(user_dir, 0755); + + GSubprocess *untar = + g_subprocess_new(G_SUBPROCESS_FLAGS_NONE, NULL, "tar", "xzf", + archive_path, "-C", user_dir, NULL); + g_assert_nonnull(untar); + g_assert_true(g_subprocess_wait_check(untar, NULL, NULL)); + g_object_unref(untar); + + char *imported_file = + g_build_filename(user_dir, "ZZZ_import_test", "1.txt", NULL); + g_assert_true(g_file_test(imported_file, G_FILE_TEST_EXISTS)); + char *contents = NULL; + g_file_get_contents(imported_file, &contents, NULL, NULL); + g_assert_nonnull(contents); + g_strchomp(contents); + g_assert_cmpstr(contents, ==, "ヽ(>∀<☆)ノ"); + g_free(contents); + + window_test_setup(); + gboolean found = FALSE; + for (guint i = 0; i < test_self->category_widgets->len; i++) { + CategoryWidgets *cw = g_ptr_array_index(test_self->category_widgets, i); + if (g_strcmp0(cw->name, "ZZZ import test") == 0) { + found = TRUE; + break; } + } + g_assert_true(found); + window_test_teardown(); - window_test_teardown (); + g_unlink(imported_file); + g_free(imported_file); + char *imported_cat = g_build_filename(user_dir, "ZZZ_import_test", NULL); + g_rmdir(imported_cat); + g_free(imported_cat); + g_rmdir(user_dir); + char *moemoji_parent = + g_build_filename(g_get_user_data_dir(), "moemoji", NULL); + g_rmdir(moemoji_parent); + g_free(moemoji_parent); + g_free(user_dir); + + g_unlink(fpath); + g_free(fpath); + g_rmdir(cat_dir); + g_free(cat_dir); + g_unlink(archive_path); + g_free(archive_path); + + GSubprocess *rm = g_subprocess_new(G_SUBPROCESS_FLAGS_NONE, NULL, "rm", "-rf", + src_dir, NULL); + if (rm) { + g_subprocess_wait(rm, NULL, NULL); + g_object_unref(rm); + } + g_free(src_dir); } -static void -test_window_initial_active_chip (void) -{ - window_test_setup (); - - g_assert_cmpint (test_self->active_chip_index, ==, -1); - - window_test_teardown (); +static void test_category_name_validation(void) { + window_test_setup(); + gtk_editable_set_text(GTK_EDITABLE(test_self->category_name_entry), ""); + g_assert_false(gtk_widget_get_sensitive(test_self->category_save_button)); + gtk_editable_set_text(GTK_EDITABLE(test_self->category_name_entry), " "); + g_assert_false(gtk_widget_get_sensitive(test_self->category_save_button)); + gtk_editable_set_text(GTK_EDITABLE(test_self->category_name_entry), + "Happy Faces"); + g_assert_true(gtk_widget_get_sensitive(test_self->category_save_button)); + gtk_editable_set_text(GTK_EDITABLE(test_self->category_name_entry), + "my-cool_category"); + g_assert_true(gtk_widget_get_sensitive(test_self->category_save_button)); + gtk_editable_set_text(GTK_EDITABLE(test_self->category_name_entry), + "bad/name"); + g_assert_false(gtk_widget_get_sensitive(test_self->category_save_button)); + gtk_editable_set_text(GTK_EDITABLE(test_self->category_name_entry), + "bad.name"); + g_assert_false(gtk_widget_get_sensitive(test_self->category_save_button)); + window_test_teardown(); } -static void -test_window_all_categories_visible (void) -{ - window_test_setup (); - - for (guint i = 0; i < test_self->category_widgets->len; i++) { - CategoryWidgets *cw = g_ptr_array_index (test_self->category_widgets, i); - g_assert_true (gtk_widget_get_visible (cw->header)); - g_assert_true (gtk_widget_get_visible (cw->flow)); - } - - window_test_teardown (); +static void test_manage_search_entry(void) { + window_test_setup(); + GtkWidget *page = + gtk_stack_get_child_by_name(test_self->view_stack, "manage"); + g_assert_nonnull(page); + GtkWidget *search = g_object_get_data(G_OBJECT(page), "manage-search"); + g_assert_nonnull(search); + g_assert_true(GTK_IS_SEARCH_ENTRY(search)); + window_test_teardown(); } -static void -test_window_bottom_spacer (void) -{ - window_test_setup (); - - g_assert_nonnull (test_self->bottom_spacer); - GtkWidget *last = gtk_widget_get_last_child (GTK_WIDGET (test_self->content_box)); - g_assert_true (last == test_self->bottom_spacer); - - window_test_teardown (); +static void test_manage_populated(void) { + window_test_setup(); + g_action_group_activate_action(G_ACTION_GROUP(test_self), "manage", NULL); + GtkWidget *page = + gtk_stack_get_child_by_name(test_self->view_stack, "manage"); + GtkWidget *content = g_object_get_data(G_OBJECT(page), "manage-content"); + g_assert_nonnull(content); + g_assert_nonnull(gtk_widget_get_first_child(content)); + window_test_teardown(); } -int -main (int argc, char *argv[]) -{ - g_setenv ("MESON_SOURCE_ROOT", SRCDIR, TRUE); - g_test_init (&argc, &argv, NULL); - - run_find_kaomoji_bogus (); - - gboolean have_display = gtk_init_check (); - - g_test_add_func ("/display-name/underscores", test_display_name_underscores); - g_test_add_func ("/display-name/no-underscores", test_display_name_no_underscores); - g_test_add_func ("/display-name/already-upper", test_display_name_already_upper); - g_test_add_func ("/display-name/single-char", test_display_name_single_char); - g_test_add_func ("/display-name/empty", test_display_name_empty); - g_test_add_func ("/find-kaomoji/with-env", test_find_kaomoji_with_env); - g_test_add_func ("/find-kaomoji/bogus", test_find_kaomoji_bogus); - g_test_add_func ("/sni/category", test_sni_category); - g_test_add_func ("/sni/id", test_sni_id); - g_test_add_func ("/sni/item-is-menu", test_sni_item_is_menu); - g_test_add_func ("/sni/menu", test_sni_menu); - g_test_add_func ("/sni/unknown", test_sni_unknown); - g_test_add_func ("/dbusmenu/version", test_dbusmenu_version); - g_test_add_func ("/dbusmenu/status", test_dbusmenu_status); - g_test_add_func ("/dbusmenu/text-direction", test_dbusmenu_text_direction); - g_test_add_func ("/dbusmenu/unknown", test_dbusmenu_unknown); - g_test_add_func ("/category-widgets/chip-init", test_category_widgets_chip_init); - if (have_display) { - g_test_add_func ("/window/chip-setup", test_window_chip_setup); - g_test_add_func ("/window/chip-labels-match", test_window_chip_labels_match); - g_test_add_func ("/window/initial-active-chip", test_window_initial_active_chip); - g_test_add_func ("/window/all-categories-visible", test_window_all_categories_visible); - g_test_add_func ("/window/bottom-spacer", test_window_bottom_spacer); - } - - return g_test_run (); +static void test_manage_dupe_name_blocked(void) { + window_test_setup(); + g_assert_cmpuint(test_self->category_widgets->len, >, 0); + CategoryWidgets *cw = g_ptr_array_index(test_self->category_widgets, 0); + gtk_editable_set_text(GTK_EDITABLE(test_self->category_name_entry), cw->name); + g_assert_false(gtk_widget_get_sensitive(test_self->category_save_button)); + window_test_teardown(); +} + +int main(int argc, char *argv[]) { + g_setenv("MESON_SOURCE_ROOT", SRCDIR, TRUE); + g_test_init(&argc, &argv, NULL); + + run_find_kaomoji_bogus(); + + gboolean have_display = gtk_init_check(); + + g_test_add_func("/display-name/underscores", test_display_name_underscores); + g_test_add_func("/display-name/no-underscores", + test_display_name_no_underscores); + g_test_add_func("/display-name/already-upper", + test_display_name_already_upper); + g_test_add_func("/display-name/single-char", test_display_name_single_char); + g_test_add_func("/display-name/empty", test_display_name_empty); + g_test_add_func("/find-kaomoji/with-env", test_find_kaomoji_with_env); + g_test_add_func("/find-kaomoji/bogus", test_find_kaomoji_bogus); + g_test_add_func("/sni/category", test_sni_category); + g_test_add_func("/sni/id", test_sni_id); + g_test_add_func("/sni/item-is-menu", test_sni_item_is_menu); + g_test_add_func("/sni/menu", test_sni_menu); + g_test_add_func("/sni/unknown", test_sni_unknown); + g_test_add_func("/dbusmenu/version", test_dbusmenu_version); + g_test_add_func("/dbusmenu/status", test_dbusmenu_status); + g_test_add_func("/dbusmenu/text-direction", test_dbusmenu_text_direction); + g_test_add_func("/dbusmenu/unknown", test_dbusmenu_unknown); + g_test_add_func("/category-widgets/chip-init", + test_category_widgets_chip_init); + if (have_display) { + g_test_add_func("/window/chip-setup", test_window_chip_setup); + g_test_add_func("/window/chip-labels-match", test_window_chip_labels_match); + g_test_add_func("/window/initial-active-chip", + test_window_initial_active_chip); + g_test_add_func("/window/all-categories-visible", + test_window_all_categories_visible); + g_test_add_func("/window/bottom-spacer", test_window_bottom_spacer); + g_test_add_func("/window/view-stack-exists", test_window_view_stack_exists); + g_test_add_func("/window/add-button-navigates", + test_window_add_button_navigates); + g_test_add_func("/window/stack-pages-exist", test_window_stack_pages_exist); + g_test_add_func("/window/category-name-validation", + test_category_name_validation); + g_test_add_func("/window/add-category-creates-dir", + test_add_category_creates_dir); + g_test_add_func("/window/add-entry-creates-file", + test_add_entry_creates_file); + g_test_add_func("/window/export-creates-archive", + test_export_creates_archive); + g_test_add_func("/window/import-extracts-and-loads", + test_import_extracts_and_loads); + g_test_add_func("/window/manage-search-entry", test_manage_search_entry); + g_test_add_func("/window/manage-populated", test_manage_populated); + g_test_add_func("/window/manage-dupe-name-blocked", + test_manage_dupe_name_blocked); + } + + return g_test_run(); }