Compare commits

...
4 Commits
6 changed files with 295 additions and 43 deletions
+19 -1
View File
@@ -3,10 +3,19 @@ Kaomoji picker. Browse a library of Japanese emoticons, click to copy, add your
![preview](preview.png)
## Install
1. Download .flatpak file from Releases: https://gitea.angeltech.jp/Angel-Technologies/MoeMoji/releases/tag/konater
2. Install with `flatpak install ./moemoji.flatpak`
Q: Why don't you upload to Flathub?
A: Couldn't be assed to follow all of their rules and go through review process.
## Global shortcut
This app uses global shortcut (Meta+Shift+E) for easy access. Press once to show the window, press twice to hide the window.
If you use KDE like me, you will be requested with global shortcut access on app launch. You can remap this shortcut to anything you want and later access this shortcut from system settings.
App starts minimized by default and is hidden to the system tray; by design you are expected to press Meta+Shift+E to invoke the window, copy the emoticon and hide the window with the same shortcut, so it doesn't get in your way.
If you use KDE like me, you will be requested with global shortcut access on app launch. You can remap this shortcut to anything you want and later you can edit this shortcut from system settings.
![image](Screenshot_20260228_021628.png)
@@ -25,6 +34,15 @@ All kaomojis are stored in a simple, accessible format:
To add your own category/emoticon, simply create a folder and a corresponding .txt file in either `$XDG_DATA_HOME/moemoji/kaomoji/` or `/usr/local/share/moemoji`.
If you use flatpak, the correct data path is `~/.var/app/net.angeltech.MoeMoji/data/`.
So adding your own emoticons would look like this:
1. `cd ~/.var/app/net.angeltech.MoeMoji/data/`
2. `mkdir -p moemoji/kaomoji`
3. `cd moemoji/kaomoji`
4. `mkdir your-category`
5. `echo emoticon > your-category/001.txt`
6. relaunch app
You can add large ASCII text files or any text you want; the app handles multiline texts and displays a neat little preview.
# Build
+120 -18
View File
@@ -62,6 +62,13 @@ static void on_popover_leave(G_GNUC_UNUSED GtkEventControllerMotion *ctrl,
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 add_kaomoji_button(GtkFlowBox *flow, const char *text) {
gboolean multiline = (strchr(text, '\n') != NULL);
const char *label_text = text;
@@ -88,6 +95,8 @@ static void add_kaomoji_button(GtkFlowBox *flow, const char *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);
@@ -139,34 +148,112 @@ static void load_category(MoeMojiWindow *self, const char *kaomoji_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_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);
}
}
static void on_chip_clicked(GtkButton *button, gpointer user_data) {
MoeMojiWindow *self = MOEMOJI_WINDOW(user_data);
const char *current = gtk_editable_get_text(GTK_EDITABLE(self->search_entry));
const char *name = gtk_button_get_label(button);
if (g_strcmp0(current, name) == 0)
gtk_editable_set_text(GTK_EDITABLE(self->search_entry), "");
else
gtk_editable_set_text(GTK_EDITABLE(self->search_entry), name);
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(GtkAdjustment *adj, gpointer user_data) {
(void)adj;
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 == NULL || query[0] == '\0')
return;
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);
gboolean visible;
if (query == NULL || query[0] == '\0') {
visible = TRUE;
} else {
char *name_lower = g_utf8_strdown(cw->name, -1);
char *query_lower = g_utf8_strdown(query, -1);
visible = (strstr(name_lower, query_lower) != NULL);
g_free(name_lower);
g_free(query_lower);
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;
}
gtk_widget_set_visible(cw->header, visible);
gtk_widget_set_visible(cw->flow, visible);
}
}
@@ -180,6 +267,8 @@ static void moemoji_window_class_init(MoeMojiWindowClass *klass) {
gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow,
search_entry);
gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, header_bar);
gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow,
kaomoji_scroll);
}
static void collect_categories(const char *base_dir, GHashTable *seen,
@@ -218,6 +307,7 @@ static void moemoji_window_init(MoeMojiWindow *self) {
gtk_widget_add_css_class(GTK_WIDGET(self->content_box), "content-area");
self->category_widgets =
g_ptr_array_new_with_free_func(category_widgets_free);
self->active_chip_index = -1;
char *kaomoji_dir = find_kaomoji_dir();
g_autofree char *user_dir =
g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL);
@@ -246,6 +336,8 @@ static void moemoji_window_init(MoeMojiWindow *self) {
}
g_ptr_array_free(entries, TRUE);
g_free(kaomoji_dir);
self->bottom_spacer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_box_append(self->content_box, self->bottom_spacer);
g_signal_connect(self->search_entry, "search-changed",
G_CALLBACK(on_search_changed), self);
if (self->category_widgets->len > 0) {
@@ -260,6 +352,7 @@ static void moemoji_window_init(MoeMojiWindow *self) {
gtk_widget_add_css_class(btn, "category-chip");
gtk_widget_add_css_class(btn, "flat");
g_signal_connect(btn, "clicked", G_CALLBACK(on_chip_clicked), self);
cw->chip = btn;
gtk_flow_box_insert(GTK_FLOW_BOX(flow), btn, -1);
}
self->category_bar = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
@@ -275,5 +368,14 @@ static void moemoji_window_init(MoeMojiWindow *self) {
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(cat_scroll), flow);
gtk_box_append(self->category_bar, cat_scroll);
gtk_box_append(self->outer_box, GTK_WIDGET(self->category_bar));
GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment(
GTK_SCROLLED_WINDOW(self->kaomoji_scroll));
g_signal_connect(vadj, "value-changed", G_CALLBACK(on_scroll_changed),
self);
g_signal_connect(vadj, "notify::page-size",
G_CALLBACK(on_page_size_changed), self);
g_signal_connect(self->kaomoji_scroll, "map",
G_CALLBACK(on_kaomoji_scroll_map), self);
}
}
+4
View File
@@ -5,6 +5,7 @@
typedef struct {
GtkWidget *header;
GtkWidget *flow;
GtkWidget *chip;
char *name;
} CategoryWidgets;
@@ -15,7 +16,10 @@ struct _MoeMojiWindow {
GtkSearchEntry *search_entry;
GtkWidget *header_bar;
GtkBox *category_bar;
GtkWidget *kaomoji_scroll;
GPtrArray *category_widgets;
GtkWidget *bottom_spacer;
int active_chip_index;
};
G_BEGIN_DECLS
+4 -4
View File
@@ -2,8 +2,8 @@
<interface>
<requires lib="gtk" version="4.0" />
<template class="MoeMojiWindow" parent="GtkApplicationWindow">
<property name="default-width">280</property>
<property name="default-height">600</property>
<property name="default-width">680</property>
<property name="default-height">400</property>
<property name="resizable">True</property>
<property name="title">MoeMoji</property>
@@ -18,14 +18,14 @@
<property name="orientation">vertical</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="placeholder-text">Filter by category...</property>
<property name="placeholder-text">Jump to category...</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-top">6</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<object class="GtkScrolledWindow" id="kaomoji_scroll">
<property name="hscrollbar-policy">never</property>
<property name="vexpand">True</property>
<child>
+5
View File
@@ -53,3 +53,8 @@ wallpaper-bg > * {
border-radius: 14px;
padding: 8px;
}
.chip-active {
background-color: rgba(255, 255, 255, 0.25);
border-radius: 8px;
}
+127 -4
View File
@@ -1,6 +1,8 @@
#include <glib.h>
#include <glib/gstdio.h>
#include <gtk/gtk.h>
#include "moemoji-internal.h"
#include "moemoji-window.h"
static void
test_display_name_underscores (void)
@@ -45,16 +47,16 @@ test_display_name_empty (void)
static void
test_find_kaomoji_with_env (void)
{
g_setenv ("MESON_SOURCE_ROOT", SRCDIR, TRUE);
char *dir = find_kaomoji_dir ();
g_assert_nonnull (dir);
g_assert_true (g_file_test (dir, G_FILE_TEST_IS_DIR));
g_free (dir);
g_unsetenv ("MESON_SOURCE_ROOT");
}
static gboolean find_kaomoji_bogus_passed = FALSE;
static void
test_find_kaomoji_bogus (void)
run_find_kaomoji_bogus (void)
{
g_setenv ("MESON_SOURCE_ROOT", "/nonexistent", TRUE);
char *saved = g_get_current_dir ();
@@ -65,7 +67,14 @@ test_find_kaomoji_bogus (void)
g_assert_true (g_chdir (saved) == 0);
g_free (saved);
g_unsetenv ("MESON_SOURCE_ROOT");
g_setenv ("MESON_SOURCE_ROOT", SRCDIR, TRUE);
find_kaomoji_bogus_passed = TRUE;
}
static void
test_find_kaomoji_bogus (void)
{
g_assert_true (find_kaomoji_bogus_passed);
}
static void
@@ -145,11 +154,117 @@ test_dbusmenu_unknown (void)
g_assert_null (v);
}
static void
test_category_widgets_chip_init (void)
{
CategoryWidgets *cw = g_new0 (CategoryWidgets, 1);
g_assert_null (cw->chip);
g_assert_null (cw->header);
g_assert_null (cw->flow);
g_assert_null (cw->name);
g_free (cw);
}
G_GNUC_INTERNAL GResource *moemoji_get_resource (void);
static GResource *test_res = NULL;
static GtkWidget *test_win = NULL;
static MoeMojiWindow *test_self = NULL;
static void
window_test_setup (void)
{
if (!test_res) {
test_res = moemoji_get_resource ();
g_resources_register (test_res);
}
test_win = g_object_new (MOEMOJI_TYPE_WINDOW, NULL);
test_self = MOEMOJI_WINDOW (test_win);
}
static void
window_test_teardown (void)
{
gtk_window_destroy (GTK_WINDOW (test_win));
test_win = NULL;
test_self = NULL;
}
static void
test_window_chip_setup (void)
{
window_test_setup ();
g_assert_cmpuint (test_self->category_widgets->len, >, 0);
for (guint i = 0; i < test_self->category_widgets->len; i++) {
CategoryWidgets *cw = g_ptr_array_index (test_self->category_widgets, i);
g_assert_nonnull (cw->chip);
}
window_test_teardown ();
}
static void
test_window_chip_labels_match (void)
{
window_test_setup ();
for (guint i = 0; i < test_self->category_widgets->len; i++) {
CategoryWidgets *cw = g_ptr_array_index (test_self->category_widgets, i);
const char *chip_label = gtk_button_get_label (GTK_BUTTON (cw->chip));
g_assert_cmpstr (chip_label, ==, cw->name);
}
window_test_teardown ();
}
static void
test_window_initial_active_chip (void)
{
window_test_setup ();
g_assert_cmpint (test_self->active_chip_index, ==, -1);
window_test_teardown ();
}
static void
test_window_all_categories_visible (void)
{
window_test_setup ();
for (guint i = 0; i < test_self->category_widgets->len; i++) {
CategoryWidgets *cw = g_ptr_array_index (test_self->category_widgets, i);
g_assert_true (gtk_widget_get_visible (cw->header));
g_assert_true (gtk_widget_get_visible (cw->flow));
}
window_test_teardown ();
}
static void
test_window_bottom_spacer (void)
{
window_test_setup ();
g_assert_nonnull (test_self->bottom_spacer);
GtkWidget *last = gtk_widget_get_last_child (GTK_WIDGET (test_self->content_box));
g_assert_true (last == test_self->bottom_spacer);
window_test_teardown ();
}
int
main (int argc, char *argv[])
{
g_setenv ("MESON_SOURCE_ROOT", SRCDIR, TRUE);
g_test_init (&argc, &argv, NULL);
run_find_kaomoji_bogus ();
gboolean have_display = gtk_init_check ();
g_test_add_func ("/display-name/underscores", test_display_name_underscores);
g_test_add_func ("/display-name/no-underscores", test_display_name_no_underscores);
g_test_add_func ("/display-name/already-upper", test_display_name_already_upper);
@@ -166,6 +281,14 @@ main (int argc, char *argv[])
g_test_add_func ("/dbusmenu/status", test_dbusmenu_status);
g_test_add_func ("/dbusmenu/text-direction", test_dbusmenu_text_direction);
g_test_add_func ("/dbusmenu/unknown", test_dbusmenu_unknown);
g_test_add_func ("/category-widgets/chip-init", test_category_widgets_chip_init);
if (have_display) {
g_test_add_func ("/window/chip-setup", test_window_chip_setup);
g_test_add_func ("/window/chip-labels-match", test_window_chip_labels_match);
g_test_add_func ("/window/initial-active-chip", test_window_initial_active_chip);
g_test_add_func ("/window/all-categories-visible", test_window_all_categories_visible);
g_test_add_func ("/window/bottom-spacer", test_window_bottom_spacer);
}
return g_test_run ();
}