1962 lines
78 KiB
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);
|
|
}
|