MoeMoji/src/moemoji-window.c

1962 lines
78 KiB
C

#include "moemoji-window.h"
#include "moemoji-config.h"
#include "moemoji-internal.h"
#include <adwaita.h>
#include <glib/gstdio.h>
#include <string.h>
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);
g_free(cw->path);
g_free(cw);
}
char *make_display_name(const char *dirname) {
char *name = g_strdup(dirname);
for (char *p = name; *p; p++) {
if (*p == '_')
*p = ' ';
}
return name;
}
static char *get_category_display_name(const char *cat_path) {
g_autofree char *name_file = g_build_filename(cat_path, ".name", NULL);
char *contents = NULL;
if (g_file_get_contents(name_file, &contents, NULL, NULL)) {
g_strchomp(contents);
if (contents[0] != '\0')
return contents;
g_free(contents);
}
g_autofree char *basename = g_path_get_basename(cat_path);
return make_display_name(basename);
}
char *find_kaomoji_dir(void) {
const char *src_dir = g_getenv("MESON_SOURCE_ROOT");
if (src_dir) {
char *dev_path = g_build_filename(src_dir, "data", "kaomoji", NULL);
if (g_file_test(dev_path, G_FILE_TEST_IS_DIR))
return dev_path;
g_free(dev_path);
}
char *cwd_path = g_build_filename("data", "kaomoji", NULL);
if (g_file_test(cwd_path, G_FILE_TEST_IS_DIR))
return cwd_path;
g_free(cwd_path);
char *installed = g_build_filename(MOEMOJI_DATADIR, "kaomoji", NULL);
if (g_file_test(installed, G_FILE_TEST_IS_DIR))
return installed;
g_free(installed);
return NULL;
}
static void on_kaomoji_clicked(GtkButton *button, gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
const char *full = g_object_get_data(G_OBJECT(button), "full-text");
const char *text = full ? full : gtk_button_get_label(button);
GdkClipboard *clipboard = gtk_widget_get_clipboard(GTK_WIDGET(button));
gdk_clipboard_set_text(clipboard, text);
AdwToast *toast = adw_toast_new("Copied!");
adw_toast_set_timeout(toast, 1);
adw_toast_overlay_add_toast(self->toast_overlay, toast);
}
static void on_popover_enter(G_GNUC_UNUSED GtkEventControllerMotion *ctrl,
G_GNUC_UNUSED double x, G_GNUC_UNUSED double y,
gpointer user_data) {
gtk_popover_popup(GTK_POPOVER(user_data));
}
static void on_popover_leave(G_GNUC_UNUSED GtkEventControllerMotion *ctrl,
gpointer user_data) {
gtk_popover_popdown(GTK_POPOVER(user_data));
}
static void on_button_destroy(GtkWidget *button,
G_GNUC_UNUSED gpointer user_data) {
GtkWidget *popover = g_object_get_data(G_OBJECT(button), "popover");
if (popover)
gtk_widget_unparent(popover);
}
static void on_context_popover_closed(GtkPopover *popover,
G_GNUC_UNUSED gpointer user_data) {
GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(popover));
if (parent) {
gtk_widget_remove_css_class(parent, "context-active");
g_object_set_data(G_OBJECT(parent), "ctx-popover", NULL);
gtk_widget_unparent(GTK_WIDGET(popover));
}
}
static void on_ctx_parent_destroy(GtkWidget *parent,
G_GNUC_UNUSED gpointer user_data) {
GtkWidget *popover = g_object_get_data(G_OBJECT(parent), "ctx-popover");
if (popover)
gtk_widget_unparent(popover);
}
static gboolean enable_popover_buttons_idle(gpointer data) {
GtkWidget *box = GTK_WIDGET(data);
for (GtkWidget *child = gtk_widget_get_first_child(box); child != NULL;
child = gtk_widget_get_next_sibling(child)) {
gtk_widget_set_can_target(child, TRUE);
}
return G_SOURCE_REMOVE;
}
static GtkWidget *ctx_add_button(GtkBox *box, const char *label,
GCallback callback, gpointer user_data) {
GtkWidget *btn = gtk_button_new_with_label(label);
gtk_widget_add_css_class(btn, "flat");
gtk_widget_set_can_target(btn, FALSE);
gtk_widget_set_focusable(btn, FALSE);
g_signal_connect(btn, "clicked", callback, user_data);
gtk_box_append(box, btn);
return btn;
}
static void show_context_popover(GtkWidget *parent, GtkWidget *box, double x,
double y) {
gtk_widget_add_css_class(parent, "context-active");
GtkWidget *old = g_object_get_data(G_OBJECT(parent), "ctx-popover");
if (old)
gtk_widget_unparent(old);
GtkWidget *popover = gtk_popover_new();
gtk_widget_set_parent(popover, parent);
gtk_widget_add_css_class(popover, "context-menu-popover");
gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE);
GdkRectangle rect = {(int)x, (int)y, 1, 1};
gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect);
gtk_popover_set_child(GTK_POPOVER(popover), box);
g_object_set_data(G_OBJECT(parent), "ctx-popover", popover);
g_signal_connect(popover, "closed", G_CALLBACK(on_context_popover_closed),
NULL);
g_signal_connect(parent, "destroy", G_CALLBACK(on_ctx_parent_destroy), NULL);
gtk_popover_popup(GTK_POPOVER(popover));
g_idle_add(enable_popover_buttons_idle, box);
}
static void rebuild_pinned_box(MoeMojiWindow *self);
static gboolean is_kaomoji_pinned(MoeMojiWindow *self, const char *text) {
g_auto(GStrv) pinned = g_settings_get_strv(self->settings, "pinned-kaomojis");
return g_strv_contains((const char *const *)pinned, text);
}
static void on_ctx_pin_emote(G_GNUC_UNUSED GSimpleAction *action,
G_GNUC_UNUSED GVariant *parameter,
gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
const char *text = self->ctx_emote_text;
if (!text || is_kaomoji_pinned(self, text))
return;
g_auto(GStrv) pinned = g_settings_get_strv(self->settings, "pinned-kaomojis");
guint len = g_strv_length(pinned);
char **new_pinned = g_new(char *, len + 2);
for (guint i = 0; i < len; i++)
new_pinned[i] = pinned[i];
new_pinned[len] = g_strdup(text);
new_pinned[len + 1] = NULL;
g_settings_set_strv(self->settings, "pinned-kaomojis",
(const char *const *)new_pinned);
g_free(new_pinned[len]);
g_free(new_pinned);
rebuild_pinned_box(self);
}
static void on_ctx_unpin_emote(G_GNUC_UNUSED GSimpleAction *action,
G_GNUC_UNUSED GVariant *parameter,
gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
const char *text = self->ctx_emote_text;
if (!text)
return;
g_auto(GStrv) pinned = g_settings_get_strv(self->settings, "pinned-kaomojis");
GPtrArray *new_arr = g_ptr_array_new();
for (guint i = 0; pinned[i]; i++) {
if (g_strcmp0(pinned[i], text) != 0)
g_ptr_array_add(new_arr, pinned[i]);
}
g_ptr_array_add(new_arr, NULL);
g_settings_set_strv(self->settings, "pinned-kaomojis",
(const char *const *)new_arr->pdata);
g_ptr_array_free(new_arr, TRUE);
rebuild_pinned_box(self);
}
static void on_ctx_action_clicked(GtkButton *button, gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
const char *action = g_object_get_data(G_OBJECT(button), "action-name");
if (!action)
return;
GtkWidget *popover =
gtk_widget_get_ancestor(GTK_WIDGET(button), GTK_TYPE_POPOVER);
g_action_group_activate_action(G_ACTION_GROUP(self), action, NULL);
if (popover)
gtk_popover_popdown(GTK_POPOVER(popover));
}
static void on_pinned_right_click(GtkGestureClick *gesture,
G_GNUC_UNUSED int n_press, double x, double y,
gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
GtkWidget *button =
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture));
const char *full_text = g_object_get_data(G_OBJECT(button), "full-text");
const char *text =
full_text ? full_text : gtk_button_get_label(GTK_BUTTON(button));
g_free(self->ctx_emote_text);
self->ctx_emote_text = g_strdup(text);
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *unpin_btn = ctx_add_button(
GTK_BOX(box), "Unpin", G_CALLBACK(on_ctx_action_clicked), self);
g_object_set_data(G_OBJECT(unpin_btn), "action-name", "ctx-unpin-emote");
show_context_popover(button, box, x, y);
}
static void on_kaomoji_right_click(GtkGestureClick *gesture,
G_GNUC_UNUSED int n_press, double x,
double y, gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
GtkWidget *button =
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture));
const char *filepath = g_object_get_data(G_OBJECT(button), "emote-path");
if (!filepath)
return;
const char *full_text = g_object_get_data(G_OBJECT(button), "full-text");
const char *emote_text =
full_text ? full_text : gtk_button_get_label(GTK_BUTTON(button));
g_free(self->ctx_emote_path);
self->ctx_emote_path = g_strdup(filepath);
g_free(self->ctx_emote_text);
self->ctx_emote_text = g_strdup(emote_text);
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *pin_btn;
if (is_kaomoji_pinned(self, emote_text)) {
pin_btn = ctx_add_button(GTK_BOX(box), "Unpin",
G_CALLBACK(on_ctx_action_clicked), self);
g_object_set_data(G_OBJECT(pin_btn), "action-name", "ctx-unpin-emote");
} else {
pin_btn = ctx_add_button(GTK_BOX(box), "Pin",
G_CALLBACK(on_ctx_action_clicked), self);
g_object_set_data(G_OBJECT(pin_btn), "action-name", "ctx-pin-emote");
}
GtkWidget *del_btn = ctx_add_button(GTK_BOX(box), "Delete",
G_CALLBACK(on_ctx_action_clicked), self);
g_object_set_data(G_OBJECT(del_btn), "action-name", "ctx-delete-emote");
show_context_popover(button, box, x, y);
}
static void add_kaomoji_button(MoeMojiWindow *self, GtkFlowBox *flow,
const char *text, const char *filepath) {
gboolean multiline = (strchr(text, '\n') != NULL);
const char *label_text = text;
g_autofree char *first_line = NULL;
if (multiline) {
const char *nl = strchr(text, '\n');
first_line = g_strndup(text, nl - text);
label_text = first_line;
}
GtkWidget *button = gtk_button_new_with_label(label_text);
gtk_widget_set_halign(button, GTK_ALIGN_FILL);
GtkWidget *label = gtk_button_get_child(GTK_BUTTON(button));
gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_END);
gtk_label_set_xalign(GTK_LABEL(label), 0.0);
gtk_widget_add_css_class(button, "kaomoji-button");
gtk_widget_add_css_class(button, "flat");
if (multiline || g_utf8_strlen(label_text, -1) > 20)
gtk_widget_add_css_class(button, "kaomoji-wide");
if (multiline)
g_object_set_data_full(G_OBJECT(button), "full-text", g_strdup(text),
g_free);
if (filepath)
g_object_set_data_full(G_OBJECT(button), "emote-path", g_strdup(filepath),
g_free);
GtkGestureClick *right_click = GTK_GESTURE_CLICK(gtk_gesture_click_new());
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(right_click), 3);
g_signal_connect(right_click, "pressed", G_CALLBACK(on_kaomoji_right_click),
self);
gtk_widget_add_controller(button, GTK_EVENT_CONTROLLER(right_click));
g_signal_connect(button, "clicked", G_CALLBACK(on_kaomoji_clicked), self);
if (multiline) {
GtkWidget *popover = gtk_popover_new();
gtk_popover_set_autohide(GTK_POPOVER(popover), FALSE);
GtkWidget *label = gtk_label_new(text);
gtk_widget_add_css_class(label, "kaomoji-preview");
gtk_popover_set_child(GTK_POPOVER(popover), label);
gtk_widget_set_parent(popover, button);
g_object_set_data(G_OBJECT(button), "popover", popover);
g_signal_connect(button, "destroy", G_CALLBACK(on_button_destroy), NULL);
GtkEventController *motion = gtk_event_controller_motion_new();
g_signal_connect(motion, "enter", G_CALLBACK(on_popover_enter), popover);
g_signal_connect(motion, "leave", G_CALLBACK(on_popover_leave), popover);
gtk_widget_add_controller(button, motion);
}
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 on_category_header_right_click(GtkGestureClick *gesture,
G_GNUC_UNUSED int n_press, double x,
double y, gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
GtkWidget *header =
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture));
const char *cat_path = g_object_get_data(G_OBJECT(header), "cat-path");
const char *cat_name = g_object_get_data(G_OBJECT(header), "cat-name");
if (!cat_path)
return;
g_free(self->ctx_cat_path);
self->ctx_cat_path = g_strdup(cat_path);
g_free(self->ctx_cat_name);
self->ctx_cat_name = g_strdup(cat_name);
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *rename_btn = ctx_add_button(
GTK_BOX(box), "Rename", G_CALLBACK(on_ctx_action_clicked), self);
g_object_set_data(G_OBJECT(rename_btn), "action-name", "ctx-rename-category");
GtkWidget *del_cat_btn = ctx_add_button(
GTK_BOX(box), "Delete", G_CALLBACK(on_ctx_action_clicked), self);
g_object_set_data(G_OBJECT(del_cat_btn), "action-name",
"ctx-delete-category");
show_context_popover(header, box, x, y);
}
static void load_category(MoeMojiWindow *self, const char *kaomoji_dir,
const char *dirname) {
char *cat_path = g_build_filename(kaomoji_dir, dirname, NULL);
GDir *dir = g_dir_open(cat_path, 0, NULL);
if (!dir) {
g_free(cat_path);
return;
}
char *display_name = get_category_display_name(cat_path);
GtkWidget *header = gtk_label_new(display_name);
gtk_widget_add_css_class(header, "category-header");
gtk_label_set_xalign(GTK_LABEL(header), 0.0);
g_object_set_data_full(G_OBJECT(header), "cat-path", g_strdup(cat_path),
g_free);
g_object_set_data_full(G_OBJECT(header), "cat-name", g_strdup(display_name),
g_free);
GtkGestureClick *cat_right_click = GTK_GESTURE_CLICK(gtk_gesture_click_new());
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(cat_right_click), 3);
g_signal_connect(cat_right_click, "pressed",
G_CALLBACK(on_category_header_right_click), self);
gtk_widget_add_controller(header, GTK_EVENT_CONTROLLER(cat_right_click));
gtk_box_append(self->content_box, header);
GtkWidget *flow = gtk_flow_box_new();
gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(flow), TRUE);
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), 4);
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;
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);
if (contents[0] != '\0')
add_kaomoji_button(self, GTK_FLOW_BOX(flow), contents, filepath);
g_free(contents);
}
}
g_ptr_array_free(files, TRUE);
gtk_box_append(self->content_box, flow);
CategoryWidgets *cw = g_new0(CategoryWidgets, 1);
cw->header = header;
cw->flow = flow;
cw->name = display_name;
cw->path = g_strdup(cat_path);
g_ptr_array_add(self->category_widgets, cw);
g_dir_close(dir);
g_free(cat_path);
}
static void set_active_chip(MoeMojiWindow *self, int index) {
if (index == self->active_chip_index)
return;
if (self->active_chip_index >= 0 &&
(guint)self->active_chip_index < self->category_widgets->len) {
CategoryWidgets *old =
g_ptr_array_index(self->category_widgets, self->active_chip_index);
if (old->chip)
gtk_widget_remove_css_class(old->chip, "chip-active");
}
self->active_chip_index = index;
if (index >= 0 && (guint)index < self->category_widgets->len) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, index);
if (cw->chip)
gtk_widget_add_css_class(cw->chip, "chip-active");
}
}
static void scroll_chip_into_view(MoeMojiWindow *self, GtkWidget *chip) {
GtkWidget *sw = gtk_widget_get_ancestor(chip, GTK_TYPE_SCROLLED_WINDOW);
if (!sw)
return;
graphene_point_t p;
GtkWidget *content = gtk_scrolled_window_get_child(GTK_SCROLLED_WINDOW(sw));
if (gtk_widget_compute_point(chip, content, &GRAPHENE_POINT_INIT(0, 0), &p)) {
GtkAdjustment *adj =
gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw));
double page = gtk_adjustment_get_page_size(adj);
double val = gtk_adjustment_get_value(adj);
int h = gtk_widget_get_height(chip);
if (p.y < val)
gtk_adjustment_set_value(adj, p.y);
else if (p.y + h > val + page)
gtk_adjustment_set_value(adj, p.y + h - page);
}
}
static void scroll_to_category(MoeMojiWindow *self, int index) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, index);
graphene_point_t p;
if (gtk_widget_compute_point(cw->header, GTK_WIDGET(self->content_box),
&GRAPHENE_POINT_INIT(0, 0), &p)) {
GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment(
GTK_SCROLLED_WINDOW(self->kaomoji_scroll));
gtk_adjustment_set_value(vadj, p.y);
}
scroll_chip_into_view(self, cw->chip);
}
static void on_chip_clicked(GtkButton *button, gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
for (guint i = 0; i < self->category_widgets->len; i++) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, i);
if (cw->chip == GTK_WIDGET(button)) {
scroll_to_category(self, (int)i);
return;
}
}
}
static void update_chip_from_scroll(MoeMojiWindow *self) {
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++) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, i);
graphene_point_t p;
if (gtk_widget_compute_point(cw->header, GTK_WIDGET(self->content_box),
&GRAPHENE_POINT_INIT(0, 0), &p)) {
if (p.y <= scroll_pos)
best = (int)i;
}
}
set_active_chip(self, best);
}
static void update_spacer_height(MoeMojiWindow *self) {
if (!self->bottom_spacer || self->category_widgets->len == 0)
return;
GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment(
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);
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);
int spacer_h = page - last_h;
if (spacer_h < 0)
spacer_h = 0;
gtk_widget_set_size_request(self->bottom_spacer, -1, spacer_h);
}
static void on_scroll_changed(G_GNUC_UNUSED GtkAdjustment *adj,
gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
update_chip_from_scroll(self);
}
static void on_page_size_changed(G_GNUC_UNUSED GtkAdjustment *adj,
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) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
update_chip_from_scroll(self);
}
static void on_search_changed(GtkSearchEntry *entry, gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
const char *query = gtk_editable_get_text(GTK_EDITABLE(entry));
if (!query || query[0] == '\0')
return;
gtk_widget_grab_focus(GTK_WIDGET(entry));
g_autofree char *query_lower = g_utf8_strdown(query, -1);
for (guint i = 0; i < self->category_widgets->len; i++) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, i);
g_autofree char *name_lower = g_utf8_strdown(cw->name, -1);
if (strstr(name_lower, query_lower) != NULL) {
scroll_to_category(self, (int)i);
return;
}
}
}
static void collect_categories(const char *base_dir, GHashTable *seen,
GPtrArray *entries) {
GDir *top = g_dir_open(base_dir, 0, NULL);
if (!top)
return;
const char *name;
while ((name = g_dir_read_name(top)) != NULL) {
char *full = g_build_filename(base_dir, name, NULL);
if (g_file_test(full, G_FILE_TEST_IS_DIR) &&
!g_hash_table_contains(seen, name)) {
g_hash_table_add(seen, g_strdup(name));
char **pair = g_new(char *, 2);
pair[0] = g_strdup(base_dir);
pair[1] = g_strdup(name);
g_ptr_array_add(entries, pair);
}
g_free(full);
}
g_dir_close(top);
}
static gint64 get_dir_time(const char *base, const char *name,
const char *attr) {
g_autofree char *path = g_build_filename(base, name, NULL);
GFile *file = g_file_new_for_path(path);
g_autofree char *attrs =
g_strconcat(attr, ",", G_FILE_ATTRIBUTE_TIME_MODIFIED, NULL);
GFileInfo *info =
g_file_query_info(file, attrs, G_FILE_QUERY_INFO_NONE, NULL, NULL);
gint64 t = 0;
if (info) {
if (g_file_info_has_attribute(info, attr))
t = g_file_info_get_attribute_uint64(info, attr);
else
t = g_file_info_get_attribute_uint64(info,
G_FILE_ATTRIBUTE_TIME_MODIFIED);
g_object_unref(info);
}
g_object_unref(file);
return t;
}
static const char *current_sort_order = "alpha-asc";
static gint compare_entries(gconstpointer a, gconstpointer b) {
char **ea = *(char ***)a;
char **eb = *(char ***)b;
const char *order = current_sort_order;
gboolean desc = g_str_has_suffix(order, "-desc");
if (g_str_has_prefix(order, "alpha")) {
g_autofree char *pa = g_build_filename(ea[0], ea[1], NULL);
g_autofree char *pb = g_build_filename(eb[0], eb[1], NULL);
g_autofree char *da = get_category_display_name(pa);
g_autofree char *db = get_category_display_name(pb);
g_autofree char *ka = g_utf8_collate_key_for_filename(da, -1);
g_autofree char *kb = g_utf8_collate_key_for_filename(db, -1);
gint r = strcmp(ka, kb);
return desc ? -r : r;
}
const char *attr = G_FILE_ATTRIBUTE_TIME_MODIFIED;
gint64 ta = get_dir_time(ea[0], ea[1], attr);
gint64 tb = get_dir_time(eb[0], eb[1], attr);
gint r = (ta > tb) - (ta < tb);
if (r == 0) {
g_autofree char *ka = g_utf8_collate_key_for_filename(ea[1], -1);
g_autofree char *kb = g_utf8_collate_key_for_filename(eb[1], -1);
r = strcmp(ka, kb);
}
return desc ? -r : r;
}
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();
if (g_file_test(user_dir, G_FILE_TEST_IS_DIR))
collect_categories(user_dir, seen, entries);
if (kaomoji_dir)
collect_categories(kaomoji_dir, seen, entries);
g_hash_table_destroy(seen);
g_ptr_array_sort(entries, compare_entries);
g_free(kaomoji_dir);
return entries;
}
static void apply_saved_order(GSettings *settings, GPtrArray *entries) {
g_auto(GStrv) saved = g_settings_get_strv(settings, "category-order");
if (!saved || !saved[0])
return;
GPtrArray *ordered = g_ptr_array_new();
gboolean *used = g_new0(gboolean, entries->len);
for (int i = 0; saved[i]; i++) {
for (guint j = 0; j < entries->len; j++) {
if (used[j])
continue;
char **pair = g_ptr_array_index(entries, j);
if (g_strcmp0(pair[1], saved[i]) == 0) {
g_ptr_array_add(ordered, pair);
used[j] = TRUE;
break;
}
}
}
for (guint j = 0; j < entries->len; j++) {
if (!used[j])
g_ptr_array_add(ordered, g_ptr_array_index(entries, j));
}
for (guint i = 0; i < ordered->len; i++)
g_ptr_array_index(entries, i) = g_ptr_array_index(ordered, i);
g_ptr_array_free(ordered, TRUE);
g_free(used);
}
static void free_category_entries(GPtrArray *entries) {
for (guint i = 0; i < entries->len; i++) {
char **pair = g_ptr_array_index(entries, i);
g_free(pair[0]);
g_free(pair[1]);
g_free(pair);
}
g_ptr_array_free(entries, TRUE);
}
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->sidebar_toggle, is_main);
gtk_widget_set_visible(self->add_button, is_main);
gtk_widget_set_visible(self->sort_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;
g_autofree char *stripped = g_strstrip(g_strdup(text));
return stripped[0] != '\0';
}
static gboolean category_name_taken(const char *name) {
g_autofree char *stripped = g_strstrip(g_strdup(name));
GPtrArray *entries = collect_all_categories();
gboolean found = FALSE;
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 = get_category_display_name(cat_path);
if (g_utf8_collate(display, stripped) == 0) {
found = TRUE;
break;
}
}
free_category_entries(entries);
return found;
}
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));
const char *error = NULL;
gboolean valid = TRUE;
if (!is_valid_category_name(text)) {
valid = FALSE;
} else if (category_name_taken(text)) {
error = "A category with this name already exists";
valid = FALSE;
}
gtk_widget_set_sensitive(self->category_save_button, valid);
gtk_label_set_text(GTK_LABEL(self->category_error_label), error ? error : "");
gtk_widget_set_visible(self->category_error_label, error != NULL);
}
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_name_taken(text))
return;
g_autofree char *stripped = g_strstrip(g_strdup(text));
g_autofree char *uuid = g_uuid_string_random();
g_autofree char *cat_path =
g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", uuid, NULL);
g_mkdir_with_parents(cat_path, 0755);
g_autofree char *name_file = g_build_filename(cat_path, ".name", NULL);
g_file_set_contents(name_file, stripped, -1, NULL);
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 *full_path = g_build_filename(pair[0], pair[1], NULL);
g_autofree char *display = get_category_display_name(full_path);
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 save_category_order(MoeMojiWindow *self) {
guint len = self->category_widgets->len;
char **order = g_new0(char *, len + 1);
for (guint i = 0; i < len; i++) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, i);
order[i] = g_path_get_basename(cw->path);
}
g_settings_set_strv(self->settings, "category-order",
(const char *const *)order);
g_strfreev(order);
}
static GdkContentProvider *on_chip_drag_prepare(GtkDragSource *source,
G_GNUC_UNUSED double x,
G_GNUC_UNUSED double y,
G_GNUC_UNUSED gpointer data) {
GtkWidget *widget =
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(source));
const char *path = g_object_get_data(G_OBJECT(widget), "cat-path");
if (!path)
return NULL;
GValue val = G_VALUE_INIT;
g_value_init(&val, G_TYPE_STRING);
g_value_set_string(&val, path);
return gdk_content_provider_new_for_value(&val);
}
static void on_chip_drag_begin(GtkDragSource *source,
G_GNUC_UNUSED GdkDrag *drag,
G_GNUC_UNUSED gpointer data) {
GtkWidget *widget =
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(source));
GdkPaintable *paintable = gtk_widget_paintable_new(widget);
gtk_drag_source_set_icon(source, paintable, 0, 0);
g_object_unref(paintable);
}
static void clear_drop_indicator(GtkWidget *chip_box) {
GtkWidget *active = g_object_get_data(G_OBJECT(chip_box), "active-drop-sep");
if (active) {
gtk_widget_set_visible(active, FALSE);
g_object_set_data(G_OBJECT(chip_box), "active-drop-sep", NULL);
}
}
static void show_drop_indicator(GtkWidget *chip, double y) {
GtkWidget *chip_box = gtk_widget_get_parent(chip);
if (!chip_box)
return;
clear_drop_indicator(chip_box);
int height = gtk_widget_get_height(chip);
GtkWidget *sep;
if (y < height / 2.0)
sep = g_object_get_data(G_OBJECT(chip), "drop-sep-before");
else
sep = g_object_get_data(G_OBJECT(chip), "drop-sep-after");
if (sep) {
gtk_widget_set_visible(sep, TRUE);
g_object_set_data(G_OBJECT(chip_box), "active-drop-sep", sep);
}
}
static gboolean reorder_chips_idle(gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
if (self->category_widgets->len == 0)
return G_SOURCE_REMOVE;
CategoryWidgets *first = g_ptr_array_index(self->category_widgets, 0);
GtkWidget *chip_box = gtk_widget_get_parent(first->chip);
if (!chip_box)
return G_SOURCE_REMOVE;
GtkWidget *chip_after = NULL;
for (guint i = 0; i < self->category_widgets->len; i++) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, i);
GtkWidget *sep = g_object_get_data(G_OBJECT(cw->chip), "drop-sep-before");
if (sep) {
gtk_widget_insert_after(sep, chip_box, chip_after);
chip_after = sep;
}
gtk_widget_insert_after(cw->chip, chip_box, chip_after);
chip_after = cw->chip;
}
CategoryWidgets *last_cw = g_ptr_array_index(self->category_widgets,
self->category_widgets->len - 1);
GtkWidget *sep_end =
g_object_get_data(G_OBJECT(last_cw->chip), "drop-sep-after");
if (sep_end)
gtk_widget_insert_after(sep_end, chip_box, chip_after);
return G_SOURCE_REMOVE;
}
static gboolean on_chip_drop(GtkDropTarget *target, const GValue *value,
G_GNUC_UNUSED double x, double y,
gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
GtkWidget *dst_widget =
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(target));
const char *src_path = g_value_get_string(value);
const char *dst_path = g_object_get_data(G_OBJECT(dst_widget), "cat-path");
if (!src_path || !dst_path || g_strcmp0(src_path, dst_path) == 0)
return FALSE;
int src_idx = -1, dst_idx = -1;
for (guint i = 0; i < self->category_widgets->len; i++) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, i);
if (g_strcmp0(cw->path, src_path) == 0)
src_idx = (int)i;
if (g_strcmp0(cw->path, dst_path) == 0)
dst_idx = (int)i;
}
if (src_idx < 0 || dst_idx < 0)
return FALSE;
int height = gtk_widget_get_height(dst_widget);
int insert_idx = (y < height / 2.0) ? dst_idx : dst_idx + 1;
if (src_idx < insert_idx)
insert_idx--;
if (insert_idx == src_idx)
return FALSE;
CategoryWidgets *moved = g_ptr_array_index(self->category_widgets, src_idx);
GPtrArray *reordered = g_ptr_array_new();
for (guint i = 0; i < self->category_widgets->len; i++) {
if ((int)i == src_idx)
continue;
if ((int)reordered->len == insert_idx)
g_ptr_array_add(reordered, moved);
g_ptr_array_add(reordered, g_ptr_array_index(self->category_widgets, i));
}
if ((int)reordered->len == insert_idx)
g_ptr_array_add(reordered, moved);
for (guint i = 0; i < reordered->len; i++)
g_ptr_array_index(self->category_widgets, i) =
g_ptr_array_index(reordered, i);
g_ptr_array_free(reordered, TRUE);
save_category_order(self);
GtkWidget *after = NULL;
for (guint i = 0; i < self->category_widgets->len; i++) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, i);
gtk_widget_insert_after(cw->header, GTK_WIDGET(self->content_box), after);
gtk_widget_insert_after(cw->flow, GTK_WIDGET(self->content_box),
cw->header);
after = cw->flow;
}
if (self->bottom_spacer)
gtk_widget_insert_after(self->bottom_spacer, GTK_WIDGET(self->content_box),
after);
GtkWidget *chip_box = gtk_widget_get_parent(dst_widget);
if (chip_box)
clear_drop_indicator(chip_box);
for (guint i = 0; i < self->category_widgets->len; i++) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, i);
if ((int)i == self->active_chip_index)
gtk_widget_add_css_class(cw->chip, "chip-active");
else
gtk_widget_remove_css_class(cw->chip, "chip-active");
}
g_idle_add(reorder_chips_idle, self);
return TRUE;
}
static GdkDragAction on_chip_drag_hover(GtkDropTarget *target,
G_GNUC_UNUSED double x, double y,
G_GNUC_UNUSED gpointer data) {
GtkWidget *widget =
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(target));
show_drop_indicator(widget, y);
return GDK_ACTION_MOVE;
}
static void on_chip_drag_leave(GtkDropTarget *target,
G_GNUC_UNUSED gpointer data) {
GtkWidget *widget =
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(target));
GtkWidget *chip_box = gtk_widget_get_parent(widget);
if (chip_box)
clear_drop_indicator(chip_box);
}
static void rebuild_pinned_box(MoeMojiWindow *self) {
if (!self->pinned_box)
return;
GtkWidget *child;
while ((child = gtk_widget_get_first_child(GTK_WIDGET(self->pinned_box))) !=
NULL)
gtk_box_remove(self->pinned_box, child);
g_auto(GStrv) pinned = g_settings_get_strv(self->settings, "pinned-kaomojis");
if (!pinned || !pinned[0]) {
gtk_widget_set_visible(GTK_WIDGET(self->pinned_box), FALSE);
return;
}
gtk_widget_set_visible(GTK_WIDGET(self->pinned_box), TRUE);
GtkWidget *header = gtk_label_new("Pinned");
gtk_widget_add_css_class(header, "pinned-header");
gtk_label_set_xalign(GTK_LABEL(header), 0.0);
gtk_box_append(self->pinned_box, header);
for (guint i = 0; pinned[i]; i++) {
const char *text = pinned[i];
gboolean multiline = (strchr(text, '\n') != NULL);
const char *label_text = text;
g_autofree char *first_line = NULL;
if (multiline) {
const char *nl = strchr(text, '\n');
first_line = g_strndup(text, nl - text);
label_text = first_line;
}
GtkWidget *btn = gtk_button_new_with_label(label_text);
gtk_widget_add_css_class(btn, "category-chip");
gtk_widget_add_css_class(btn, "flat");
gtk_button_set_has_frame(GTK_BUTTON(btn), FALSE);
GtkWidget *label = gtk_button_get_child(GTK_BUTTON(btn));
gtk_label_set_xalign(GTK_LABEL(label), 0.0);
gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_END);
if (multiline)
g_object_set_data_full(G_OBJECT(btn), "full-text", g_strdup(text),
g_free);
g_signal_connect(btn, "clicked", G_CALLBACK(on_kaomoji_clicked), self);
GtkGestureClick *rc = GTK_GESTURE_CLICK(gtk_gesture_click_new());
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(rc), 3);
g_signal_connect(rc, "pressed", G_CALLBACK(on_pinned_right_click), self);
gtk_widget_add_controller(btn, GTK_EVENT_CONTROLLER(rc));
gtk_box_append(self->pinned_box, btn);
}
}
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_paned_set_start_child(self->paned, NULL);
self->category_bar = NULL;
}
gtk_widget_set_visible(self->sidebar_toggle, FALSE);
GPtrArray *entries = collect_all_categories();
apply_saved_order(self->settings, entries);
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);
if (self->category_widgets->len > 0) {
GtkWidget *chip_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
for (guint i = 0; i < self->category_widgets->len; i++) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, i);
GtkWidget *btn = gtk_button_new_with_label(cw->name);
gtk_widget_add_css_class(btn, "category-chip");
gtk_widget_add_css_class(btn, "flat");
gtk_button_set_has_frame(GTK_BUTTON(btn), FALSE);
GtkWidget *chip_label = gtk_button_get_child(GTK_BUTTON(btn));
gtk_label_set_xalign(GTK_LABEL(chip_label), 0.0);
gtk_label_set_ellipsize(GTK_LABEL(chip_label), PANGO_ELLIPSIZE_END);
g_object_set_data_full(G_OBJECT(btn), "cat-path", g_strdup(cw->path),
g_free);
g_object_set_data_full(G_OBJECT(btn), "cat-name", g_strdup(cw->name),
g_free);
GtkGestureClick *rc = GTK_GESTURE_CLICK(gtk_gesture_click_new());
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(rc), 3);
g_signal_connect(rc, "pressed",
G_CALLBACK(on_category_header_right_click), self);
gtk_widget_add_controller(btn, GTK_EVENT_CONTROLLER(rc));
g_signal_connect(btn, "clicked", G_CALLBACK(on_chip_clicked), self);
GtkDragSource *src = gtk_drag_source_new();
gtk_drag_source_set_actions(src, GDK_ACTION_MOVE);
g_signal_connect(src, "prepare", G_CALLBACK(on_chip_drag_prepare), self);
g_signal_connect(src, "drag-begin", G_CALLBACK(on_chip_drag_begin), self);
gtk_widget_add_controller(btn, GTK_EVENT_CONTROLLER(src));
GtkDropTarget *tgt = gtk_drop_target_new(G_TYPE_STRING, GDK_ACTION_MOVE);
g_signal_connect(tgt, "drop", G_CALLBACK(on_chip_drop), self);
g_signal_connect(tgt, "enter", G_CALLBACK(on_chip_drag_hover), self);
g_signal_connect(tgt, "motion", G_CALLBACK(on_chip_drag_hover), self);
g_signal_connect(tgt, "leave", G_CALLBACK(on_chip_drag_leave), self);
gtk_widget_add_controller(btn, GTK_EVENT_CONTROLLER(tgt));
cw->chip = btn;
GtkWidget *sep = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(sep, "chip-drop-line");
gtk_widget_set_size_request(sep, -1, 2);
gtk_widget_set_visible(sep, FALSE);
gtk_box_append(GTK_BOX(chip_box), sep);
g_object_set_data(G_OBJECT(btn), "drop-sep-before", sep);
if (i > 0) {
CategoryWidgets *prev_cw =
g_ptr_array_index(self->category_widgets, i - 1);
g_object_set_data(G_OBJECT(prev_cw->chip), "drop-sep-after", sep);
}
gtk_box_append(GTK_BOX(chip_box), btn);
if (i == self->category_widgets->len - 1) {
GtkWidget *tail = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(tail, "chip-drop-line");
gtk_widget_set_size_request(tail, -1, 2);
gtk_widget_set_visible(tail, FALSE);
gtk_box_append(GTK_BOX(chip_box), tail);
g_object_set_data(G_OBJECT(btn), "drop-sep-after", tail);
}
}
self->category_bar = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
gtk_widget_add_css_class(GTK_WIDGET(self->category_bar), "category-bar");
GtkWidget *cat_scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(cat_scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(cat_scroll, TRUE);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(cat_scroll), chip_box);
gtk_box_append(self->category_bar, cat_scroll);
self->pinned_box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 2));
gtk_widget_add_css_class(GTK_WIDGET(self->pinned_box), "pinned-section");
gtk_box_append(self->category_bar, GTK_WIDGET(self->pinned_box));
rebuild_pinned_box(self);
gtk_paned_set_start_child(self->paned, GTK_WIDGET(self->category_bar));
gtk_widget_set_visible(self->sidebar_toggle, TRUE);
gtk_widget_set_visible(
GTK_WIDGET(self->category_bar),
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(self->sidebar_toggle)));
GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment(
GTK_SCROLLED_WINDOW(self->kaomoji_scroll));
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_error_label = gtk_label_new("");
gtk_widget_add_css_class(self->category_error_label, "error-label");
gtk_widget_set_visible(self->category_error_label, FALSE);
gtk_box_append(GTK_BOX(box), self->category_error_label);
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(void) {
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);
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 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");
const char *text = g_object_get_data(G_OBJECT(dialog), "emote-text");
g_unlink(filepath);
if (text && is_kaomoji_pinned(self, text)) {
g_auto(GStrv) pinned =
g_settings_get_strv(self->settings, "pinned-kaomojis");
GPtrArray *arr = g_ptr_array_new();
for (guint i = 0; pinned[i]; i++) {
if (g_strcmp0(pinned[i], text) != 0)
g_ptr_array_add(arr, pinned[i]);
}
g_ptr_array_add(arr, NULL);
g_settings_set_strv(self->settings, "pinned-kaomojis",
(const char *const *)arr->pdata);
g_ptr_array_free(arr, TRUE);
}
reload_categories(self);
}
}
static void on_ctx_delete_emote(G_GNUC_UNUSED GSimpleAction *action,
G_GNUC_UNUSED GVariant *parameter,
gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
const char *filepath = self->ctx_emote_path;
const char *emote_text = self->ctx_emote_text;
g_autofree char *body = NULL;
if (g_utf8_strlen(emote_text, -1) > 40) {
char *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);
g_object_set_data_full(G_OBJECT(confirm), "emote-text", g_strdup(emote_text),
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);
}
}
static void on_rename_entry_changed(GtkEditable *editable, gpointer user_data) {
AdwAlertDialog *dialog = ADW_ALERT_DIALOG(user_data);
const char *text = gtk_editable_get_text(editable);
const char *original = g_object_get_data(G_OBJECT(dialog), "original-name");
g_autofree char *stripped = g_strstrip(g_strdup(text));
const char *error = NULL;
gboolean valid = TRUE;
if (stripped[0] == '\0') {
error = "Name cannot be empty";
valid = FALSE;
} else if (g_strcmp0(stripped, original) == 0) {
error = NULL;
valid = FALSE;
} else if (category_name_taken(stripped)) {
error = "A category with this name already exists";
valid = FALSE;
}
adw_alert_dialog_set_body(dialog, error ? error : "");
adw_alert_dialog_set_response_enabled(dialog, "rename", valid);
}
static void on_rename_entry_activate(G_GNUC_UNUSED GtkEntry *entry,
gpointer user_data) {
AdwAlertDialog *dialog = ADW_ALERT_DIALOG(user_data);
if (adw_alert_dialog_get_response_enabled(dialog, "rename")) {
g_signal_emit_by_name(dialog, "response", "rename");
adw_dialog_force_close(ADW_DIALOG(dialog));
}
}
static void on_rename_category_response(AdwAlertDialog *dialog,
const char *response,
gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
if (g_strcmp0(response, "rename") != 0)
return;
const char *cat_path = g_object_get_data(G_OBJECT(dialog), "cat-path");
GtkEditable *entry =
GTK_EDITABLE(g_object_get_data(G_OBJECT(dialog), "name-entry"));
const char *raw = gtk_editable_get_text(entry);
g_autofree char *stripped = g_strstrip(g_strdup(raw));
if (stripped[0] == '\0')
return;
g_autofree char *name_file = g_build_filename(cat_path, ".name", NULL);
g_file_set_contents(name_file, stripped, -1, NULL);
for (guint i = 0; i < self->category_widgets->len; i++) {
CategoryWidgets *cw = g_ptr_array_index(self->category_widgets, i);
if (g_strcmp0(cw->path, cat_path) == 0) {
g_free(cw->name);
cw->name = g_strdup(stripped);
gtk_label_set_text(GTK_LABEL(cw->header), stripped);
if (cw->chip) {
GtkWidget *chip_label = gtk_button_get_child(GTK_BUTTON(cw->chip));
gtk_label_set_text(GTK_LABEL(chip_label), stripped);
g_object_set_data_full(G_OBJECT(cw->chip), "cat-name",
g_strdup(stripped), g_free);
}
break;
}
}
}
static void on_ctx_rename_category(G_GNUC_UNUSED GSimpleAction *action,
G_GNUC_UNUSED GVariant *parameter,
gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
const char *cat_path = self->ctx_cat_path;
const char *cat_name = self->ctx_cat_name;
AdwAlertDialog *dialog =
ADW_ALERT_DIALOG(adw_alert_dialog_new("Rename Category", NULL));
adw_alert_dialog_add_responses(dialog, "cancel", "Cancel", "rename", "Rename",
NULL);
adw_alert_dialog_set_response_appearance(dialog, "rename",
ADW_RESPONSE_SUGGESTED);
adw_alert_dialog_set_default_response(dialog, "rename");
adw_alert_dialog_set_close_response(dialog, "cancel");
adw_alert_dialog_set_response_enabled(dialog, "rename", FALSE);
GtkWidget *entry = gtk_entry_new();
gtk_editable_set_text(GTK_EDITABLE(entry), cat_name);
adw_alert_dialog_set_extra_child(dialog, entry);
g_signal_connect_swapped(dialog, "map", G_CALLBACK(gtk_widget_grab_focus),
entry);
g_signal_connect(entry, "activate", G_CALLBACK(on_rename_entry_activate),
dialog);
g_signal_connect(entry, "changed", G_CALLBACK(on_rename_entry_changed),
dialog);
g_object_set_data_full(G_OBJECT(dialog), "cat-path", g_strdup(cat_path),
g_free);
g_object_set_data_full(G_OBJECT(dialog), "original-name", g_strdup(cat_name),
g_free);
g_object_set_data(G_OBJECT(dialog), "name-entry", entry);
g_signal_connect(dialog, "response", G_CALLBACK(on_rename_category_response),
self);
adw_alert_dialog_choose(dialog, GTK_WIDGET(self), NULL, NULL, NULL);
}
static void on_ctx_delete_category(G_GNUC_UNUSED GSimpleAction *action,
G_GNUC_UNUSED GVariant *parameter,
gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
const char *cat_path = self->ctx_cat_path;
const char *cat_name = self->ctx_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 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 GtkFileDialog *new_targz_dialog(const char *title) {
GtkFileDialog *dialog = gtk_file_dialog_new();
gtk_file_dialog_set_title(dialog, title);
GtkFileFilter *filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, "Tar archives (*.tar.gz)");
gtk_file_filter_add_pattern(filter, "*.tar.gz");
GListStore *filters = g_list_store_new(GTK_TYPE_FILE_FILTER);
g_list_store_append(filters, filter);
g_object_unref(filter);
gtk_file_dialog_set_filters(dialog, G_LIST_MODEL(filters));
g_object_unref(filters);
return dialog;
}
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 = new_targz_dialog("Export Kaomojis");
gtk_file_dialog_set_initial_name(dialog, "moemoji-export.tar.gz");
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 = new_targz_dialog("Import Kaomojis");
gtk_file_dialog_open(dialog, GTK_WINDOW(self), NULL, on_import_open_ready,
self);
g_object_unref(dialog);
}
static void update_sort_button_icon(MoeMojiWindow *self) {
g_autofree char *order =
g_settings_get_string(self->settings, "category-sort");
const char *icon = g_str_has_suffix(order, "-desc")
? "view-sort-descending-symbolic"
: "view-sort-ascending-symbolic";
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(self->sort_button), icon);
}
static void on_sort_order_changed(GSimpleAction *action, GVariant *parameter,
gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
g_simple_action_set_state(action, parameter);
const char *order = g_variant_get_string(parameter, NULL);
g_settings_set_string(self->settings, "category-sort", order);
current_sort_order = order;
update_sort_button_icon(self);
const char *empty[] = {NULL};
g_settings_set_strv(self->settings, "category-order", empty);
reload_categories(self);
save_category_order(self);
}
static void on_restore_default_order(G_GNUC_UNUSED GSimpleAction *action,
G_GNUC_UNUSED GVariant *parameter,
gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
g_settings_reset(self->settings, "category-order");
reload_categories(self);
}
static void on_sidebar_toggled(GtkToggleButton *toggle, gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
gboolean active = gtk_toggle_button_get_active(toggle);
GtkWidget *start = gtk_paned_get_start_child(self->paned);
if (start)
gtk_widget_set_visible(start, active);
gtk_button_set_icon_name(GTK_BUTTON(toggle),
active ? "pan-start-symbolic" : "pan-end-symbolic");
}
#define SIDEBAR_HIDE_WIDTH 450
static void on_surface_layout(G_GNUC_UNUSED GdkSurface *surface, int width,
G_GNUC_UNUSED int height, gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
if (width > 0 && width < SIDEBAR_HIDE_WIDTH)
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->sidebar_toggle),
FALSE);
}
static void on_window_realize(GtkWidget *widget,
G_GNUC_UNUSED gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(widget);
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(self));
if (surface)
g_signal_connect(surface, "layout", G_CALLBACK(on_surface_layout), self);
}
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, paned);
gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, right_pane);
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,
kaomoji_scroll);
gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, view_stack);
gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow,
toast_overlay);
}
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->content_box), "content-area");
gtk_paned_set_position(self->paned, 150);
GtkWidget *empty_titlebar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_visible(empty_titlebar, FALSE);
gtk_window_set_titlebar(GTK_WINDOW(self), empty_titlebar);
self->header_bar = GTK_WIDGET(adw_header_bar_new());
gtk_widget_add_css_class(self->header_bar, "flat");
adw_header_bar_set_show_start_title_buttons(ADW_HEADER_BAR(self->header_bar),
FALSE);
adw_header_bar_set_show_end_title_buttons(ADW_HEADER_BAR(self->header_bar),
TRUE);
self->settings = g_settings_new("jp.angeltech.MoeMoji");
self->sidebar_toggle = GTK_WIDGET(gtk_toggle_button_new());
gtk_button_set_icon_name(GTK_BUTTON(self->sidebar_toggle),
"pan-start-symbolic");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->sidebar_toggle), TRUE);
adw_header_bar_pack_start(ADW_HEADER_BAR(self->header_bar),
self->sidebar_toggle);
self->add_button = gtk_button_new_from_icon_name("list-add-symbolic");
adw_header_bar_pack_start(ADW_HEADER_BAR(self->header_bar), self->add_button);
self->sort_button = GTK_WIDGET(gtk_menu_button_new());
GMenu *sort_menu = g_menu_new();
g_menu_append(sort_menu, "Alphabetically ↑", "win.sort-order::alpha-asc");
g_menu_append(sort_menu, "Alphabetically ↓", "win.sort-order::alpha-desc");
g_menu_append(sort_menu, "Modified ↑", "win.sort-order::modified-asc");
g_menu_append(sort_menu, "Modified ↓", "win.sort-order::modified-desc");
g_menu_append(sort_menu, "Default order", "win.restore-default-order");
gtk_menu_button_set_menu_model(GTK_MENU_BUTTON(self->sort_button),
G_MENU_MODEL(sort_menu));
g_object_unref(sort_menu);
adw_header_bar_pack_start(ADW_HEADER_BAR(self->header_bar),
self->sort_button);
g_autofree char *saved_order =
g_settings_get_string(self->settings, "category-sort");
GSimpleAction *sort_action = g_simple_action_new_stateful(
"sort-order", G_VARIANT_TYPE_STRING, g_variant_new_string(saved_order));
g_signal_connect(sort_action, "change-state",
G_CALLBACK(on_sort_order_changed), self);
g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(sort_action));
GSimpleAction *restore_action =
g_simple_action_new("restore-default-order", NULL);
g_signal_connect(restore_action, "activate",
G_CALLBACK(on_restore_default_order), self);
g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(restore_action));
g_object_unref(restore_action);
current_sort_order =
g_variant_get_string(g_action_get_state(G_ACTION(sort_action)), NULL);
update_sort_button_icon(self);
g_object_unref(sort_action);
self->back_button = gtk_button_new_from_icon_name("go-previous-symbolic");
gtk_widget_set_visible(self->back_button, FALSE);
adw_header_bar_pack_start(ADW_HEADER_BAR(self->header_bar),
self->back_button);
self->menu_button = GTK_WIDGET(gtk_menu_button_new());
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(self->menu_button),
"open-menu-symbolic");
GMenu *primary_menu = g_menu_new();
g_menu_append(primary_menu, "Export…", "win.export");
g_menu_append(primary_menu, "Import…", "win.import");
gtk_menu_button_set_menu_model(GTK_MENU_BUTTON(self->menu_button),
G_MENU_MODEL(primary_menu));
g_object_unref(primary_menu);
adw_header_bar_pack_end(ADW_HEADER_BAR(self->header_bar), self->menu_button);
gtk_box_prepend(self->right_pane, self->header_bar);
self->category_widgets =
g_ptr_array_new_with_free_func(category_widgets_free);
self->active_chip_index = -1;
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 *ctx_rename_cat =
g_simple_action_new("ctx-rename-category", NULL);
g_signal_connect(ctx_rename_cat, "activate",
G_CALLBACK(on_ctx_rename_category), self);
g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(ctx_rename_cat));
g_object_unref(ctx_rename_cat);
GSimpleAction *ctx_delete_cat =
g_simple_action_new("ctx-delete-category", NULL);
g_signal_connect(ctx_delete_cat, "activate",
G_CALLBACK(on_ctx_delete_category), self);
g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(ctx_delete_cat));
g_object_unref(ctx_delete_cat);
GSimpleAction *ctx_delete_emote =
g_simple_action_new("ctx-delete-emote", NULL);
g_signal_connect(ctx_delete_emote, "activate",
G_CALLBACK(on_ctx_delete_emote), self);
g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(ctx_delete_emote));
g_object_unref(ctx_delete_emote);
GSimpleAction *ctx_pin = g_simple_action_new("ctx-pin-emote", NULL);
g_signal_connect(ctx_pin, "activate", G_CALLBACK(on_ctx_pin_emote), self);
g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(ctx_pin));
g_object_unref(ctx_pin);
GSimpleAction *ctx_unpin = g_simple_action_new("ctx-unpin-emote", NULL);
g_signal_connect(ctx_unpin, "activate", G_CALLBACK(on_ctx_unpin_emote), self);
g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(ctx_unpin));
g_object_unref(ctx_unpin);
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();
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");
reload_categories(self);
g_signal_connect(self->sidebar_toggle, "toggled",
G_CALLBACK(on_sidebar_toggled), self);
g_signal_connect(self, "realize", G_CALLBACK(on_window_realize), NULL);
gtk_search_entry_set_key_capture_widget(self->search_entry, GTK_WIDGET(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);
}