init
This commit is contained in:
+46
@@ -0,0 +1,46 @@
|
||||
#include <glib/gi18n.h>
|
||||
#include <adwaita.h>
|
||||
|
||||
#include "moemoji-config.h"
|
||||
#include "moemoji-application.h"
|
||||
|
||||
static void register_with_portal (void) {
|
||||
GError *error = NULL;
|
||||
GDBusConnection *bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
|
||||
if (bus == NULL) {
|
||||
g_warning ("portal register: can't get session bus: %s", error->message);
|
||||
g_error_free (error);
|
||||
return;
|
||||
}
|
||||
GVariantBuilder options;
|
||||
g_variant_builder_init (&options, G_VARIANT_TYPE ("a{sv}"));
|
||||
GVariant *result = g_dbus_connection_call_sync (
|
||||
bus,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.host.portal.Registry",
|
||||
"Register",
|
||||
g_variant_new ("(s@a{sv})",
|
||||
"net.angeltech.MoeMoji",
|
||||
g_variant_builder_end (&options)),
|
||||
NULL,
|
||||
G_DBUS_CALL_FLAGS_NONE,
|
||||
-1, NULL, &error);
|
||||
if (result == NULL) {
|
||||
g_info ("portal register: %s", error->message);
|
||||
g_error_free (error);
|
||||
} else {
|
||||
g_variant_unref (result);
|
||||
}
|
||||
g_object_unref (bus);
|
||||
}
|
||||
|
||||
int main (int argc, char *argv[]) {
|
||||
bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
|
||||
bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
|
||||
textdomain(GETTEXT_PACKAGE);
|
||||
register_with_portal ();
|
||||
g_autoptr(MoeMojiApplication) app =
|
||||
moemoji_application_new("net.angeltech.MoeMoji", G_APPLICATION_DEFAULT_FLAGS);
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
moemoji_sources = [
|
||||
'main.c',
|
||||
'moemoji-window.c',
|
||||
'moemoji-application.c',
|
||||
|
||||
]
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
|
||||
moemoji_deps = [
|
||||
dependency('libadwaita-1', version: '>= 1.0'),
|
||||
cc.find_library('m', required: false),
|
||||
]
|
||||
|
||||
gnome = import('gnome')
|
||||
|
||||
moemoji_sources += gnome.compile_resources('moemoji-resources',
|
||||
'moemoji.gresource.xml',
|
||||
c_name: 'moemoji'
|
||||
)
|
||||
|
||||
executable('moemoji', moemoji_sources,
|
||||
dependencies: moemoji_deps,
|
||||
install: true,
|
||||
)
|
||||
@@ -0,0 +1,611 @@
|
||||
#include "moemoji-application.h"
|
||||
#include "moemoji-config.h"
|
||||
#include "moemoji-internal.h"
|
||||
#include "moemoji-window.h"
|
||||
#include <unistd.h>
|
||||
|
||||
G_DEFINE_TYPE(MoeMojiApplication, moemoji_application, ADW_TYPE_APPLICATION)
|
||||
|
||||
static const gchar sni_introspection_xml[] =
|
||||
"<node>"
|
||||
" <interface name='org.kde.StatusNotifierItem'>"
|
||||
" <method name='Activate'>"
|
||||
" <arg type='i' name='x' direction='in'/>"
|
||||
" <arg type='i' name='y' direction='in'/>"
|
||||
" </method>"
|
||||
" <method name='SecondaryActivate'>"
|
||||
" <arg type='i' name='x' direction='in'/>"
|
||||
" <arg type='i' name='y' direction='in'/>"
|
||||
" </method>"
|
||||
" <method name='ContextMenu'>"
|
||||
" <arg type='i' name='x' direction='in'/>"
|
||||
" <arg type='i' name='y' direction='in'/>"
|
||||
" </method>"
|
||||
" <property name='Category' type='s' access='read'/>"
|
||||
" <property name='Id' type='s' access='read'/>"
|
||||
" <property name='Title' type='s' access='read'/>"
|
||||
" <property name='Status' type='s' access='read'/>"
|
||||
" <property name='IconName' type='s' access='read'/>"
|
||||
" <property name='ItemIsMenu' type='b' access='read'/>"
|
||||
" <property name='Menu' type='o' access='read'/>"
|
||||
" <property name='IconThemePath' type='s' access='read'/>"
|
||||
" <signal name='NewIcon'/>"
|
||||
" </interface>"
|
||||
"</node>";
|
||||
|
||||
static const gchar dbusmenu_introspection_xml[] =
|
||||
"<node>"
|
||||
" <interface name='com.canonical.dbusmenu'>"
|
||||
" <method name='GetLayout'>"
|
||||
" <arg type='i' name='parentId' direction='in'/>"
|
||||
" <arg type='i' name='recursionDepth' direction='in'/>"
|
||||
" <arg type='as' name='propertyNames' direction='in'/>"
|
||||
" <arg type='u' name='revision' direction='out'/>"
|
||||
" <arg type='(ia{sv}av)' name='layout' direction='out'/>"
|
||||
" </method>"
|
||||
" <method name='GetGroupProperties'>"
|
||||
" <arg type='ai' name='ids' direction='in'/>"
|
||||
" <arg type='as' name='propertyNames' direction='in'/>"
|
||||
" <arg type='a(ia{sv})' name='properties' direction='out'/>"
|
||||
" </method>"
|
||||
" <method name='GetProperty'>"
|
||||
" <arg type='i' name='id' direction='in'/>"
|
||||
" <arg type='s' name='name' direction='in'/>"
|
||||
" <arg type='v' name='value' direction='out'/>"
|
||||
" </method>"
|
||||
" <method name='Event'>"
|
||||
" <arg type='i' name='id' direction='in'/>"
|
||||
" <arg type='s' name='eventId' direction='in'/>"
|
||||
" <arg type='v' name='data' direction='in'/>"
|
||||
" <arg type='u' name='timestamp' direction='in'/>"
|
||||
" </method>"
|
||||
" <method name='EventGroup'>"
|
||||
" <arg type='a(isvu)' name='events' direction='in'/>"
|
||||
" <arg type='ai' name='idErrors' direction='out'/>"
|
||||
" </method>"
|
||||
" <method name='AboutToShow'>"
|
||||
" <arg type='i' name='id' direction='in'/>"
|
||||
" <arg type='b' name='needUpdate' direction='out'/>"
|
||||
" </method>"
|
||||
" <signal name='ItemsPropertiesUpdated'>"
|
||||
" <arg type='a(ia{sv})' name='updatedProps'/>"
|
||||
" <arg type='a(ias)' name='removedProps'/>"
|
||||
" </signal>"
|
||||
" <signal name='LayoutUpdated'>"
|
||||
" <arg type='u' name='revision'/>"
|
||||
" <arg type='i' name='parent'/>"
|
||||
" </signal>"
|
||||
" <property name='Version' type='u' access='read'/>"
|
||||
" <property name='TextDirection' type='s' access='read'/>"
|
||||
" <property name='Status' type='s' access='read'/>"
|
||||
" <property name='IconThemePath' type='as' access='read'/>"
|
||||
" </interface>"
|
||||
"</node>";
|
||||
|
||||
static void toggle_window(MoeMojiApplication *self) {
|
||||
if (self->window == NULL)
|
||||
return;
|
||||
GtkWidget *win = GTK_WIDGET(self->window);
|
||||
if (gtk_widget_get_visible(win)) {
|
||||
gtk_widget_set_visible(win, FALSE);
|
||||
} else {
|
||||
gtk_window_present(GTK_WINDOW(win));
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean on_close_request(GtkWindow *window, gpointer user_data) {
|
||||
(void)user_data;
|
||||
gtk_widget_set_visible(GTK_WIDGET(window), FALSE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void dbusmenu_method_call(G_GNUC_UNUSED GDBusConnection *connection,
|
||||
G_GNUC_UNUSED const gchar *sender,
|
||||
G_GNUC_UNUSED const gchar *object_path,
|
||||
G_GNUC_UNUSED const gchar *interface_name,
|
||||
const gchar *method_name, GVariant *parameters,
|
||||
GDBusMethodInvocation *invocation,
|
||||
gpointer user_data) {
|
||||
MoeMojiApplication *self = MOEMOJI_APPLICATION(user_data);
|
||||
|
||||
if (g_strcmp0(method_name, "GetLayout") == 0) {
|
||||
GVariantBuilder root_props;
|
||||
g_variant_builder_init(&root_props, G_VARIANT_TYPE("a{sv}"));
|
||||
g_variant_builder_add(&root_props, "{sv}", "children-display",
|
||||
g_variant_new_string("submenu"));
|
||||
|
||||
GVariantBuilder quit_props;
|
||||
g_variant_builder_init(&quit_props, G_VARIANT_TYPE("a{sv}"));
|
||||
g_variant_builder_add(&quit_props, "{sv}", "label",
|
||||
g_variant_new_string("Quit"));
|
||||
g_variant_builder_add(&quit_props, "{sv}", "icon-name",
|
||||
g_variant_new_string("application-exit"));
|
||||
|
||||
GVariantBuilder quit_children;
|
||||
g_variant_builder_init(&quit_children, G_VARIANT_TYPE("av"));
|
||||
|
||||
GVariant *quit_item =
|
||||
g_variant_new("(ia{sv}av)", 1, &quit_props, &quit_children);
|
||||
|
||||
GVariantBuilder root_children;
|
||||
g_variant_builder_init(&root_children, G_VARIANT_TYPE("av"));
|
||||
g_variant_builder_add(&root_children, "v", quit_item);
|
||||
|
||||
GVariant *layout =
|
||||
g_variant_new("(ia{sv}av)", 0, &root_props, &root_children);
|
||||
|
||||
g_dbus_method_invocation_return_value(
|
||||
invocation, g_variant_new("(u@(ia{sv}av))", 1, layout));
|
||||
|
||||
} else if (g_strcmp0(method_name, "GetGroupProperties") == 0) {
|
||||
GVariantBuilder builder;
|
||||
g_variant_builder_init(&builder, G_VARIANT_TYPE("a(ia{sv})"));
|
||||
g_dbus_method_invocation_return_value(
|
||||
invocation,
|
||||
g_variant_new("(@a(ia{sv}))", g_variant_builder_end(&builder)));
|
||||
|
||||
} else if (g_strcmp0(method_name, "GetProperty") == 0) {
|
||||
g_dbus_method_invocation_return_value(
|
||||
invocation, g_variant_new("(v)", g_variant_new_string("")));
|
||||
|
||||
} else if (g_strcmp0(method_name, "Event") == 0) {
|
||||
gint32 id;
|
||||
const gchar *event_id;
|
||||
g_variant_get(parameters, "(i&sv@u)", &id, &event_id, NULL, NULL);
|
||||
|
||||
if (id == 1 && g_strcmp0(event_id, "clicked") == 0) {
|
||||
g_application_quit(G_APPLICATION(self));
|
||||
}
|
||||
g_dbus_method_invocation_return_value(invocation, NULL);
|
||||
|
||||
} else if (g_strcmp0(method_name, "EventGroup") == 0) {
|
||||
GVariant *events = g_variant_get_child_value(parameters, 0);
|
||||
GVariantIter iter;
|
||||
g_variant_iter_init(&iter, events);
|
||||
gint32 id;
|
||||
const gchar *event_id;
|
||||
while (g_variant_iter_next(&iter, "(i&svu)", &id, &event_id, NULL, NULL)) {
|
||||
if (id == 1 && g_strcmp0(event_id, "clicked") == 0) {
|
||||
g_application_quit(G_APPLICATION(self));
|
||||
}
|
||||
}
|
||||
g_variant_unref(events);
|
||||
|
||||
GVariantBuilder errors;
|
||||
g_variant_builder_init(&errors, G_VARIANT_TYPE("ai"));
|
||||
g_dbus_method_invocation_return_value(
|
||||
invocation, g_variant_new("(@ai)", g_variant_builder_end(&errors)));
|
||||
|
||||
} else if (g_strcmp0(method_name, "AboutToShow") == 0) {
|
||||
g_dbus_method_invocation_return_value(invocation,
|
||||
g_variant_new("(b)", FALSE));
|
||||
|
||||
} else {
|
||||
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
|
||||
G_DBUS_ERROR_UNKNOWN_METHOD,
|
||||
"Unknown method: %s", method_name);
|
||||
}
|
||||
}
|
||||
|
||||
GVariant *dbusmenu_get_property(G_GNUC_UNUSED GDBusConnection *connection,
|
||||
G_GNUC_UNUSED const gchar *sender,
|
||||
G_GNUC_UNUSED const gchar *object_path,
|
||||
G_GNUC_UNUSED const gchar *interface_name,
|
||||
const gchar *property_name,
|
||||
G_GNUC_UNUSED GError **error,
|
||||
G_GNUC_UNUSED gpointer user_data) {
|
||||
if (g_strcmp0(property_name, "Version") == 0)
|
||||
return g_variant_new_uint32(3);
|
||||
if (g_strcmp0(property_name, "TextDirection") == 0)
|
||||
return g_variant_new_string("ltr");
|
||||
if (g_strcmp0(property_name, "Status") == 0)
|
||||
return g_variant_new_string("normal");
|
||||
if (g_strcmp0(property_name, "IconThemePath") == 0)
|
||||
return g_variant_new_strv(NULL, 0);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const GDBusInterfaceVTable dbusmenu_vtable = {
|
||||
.method_call = dbusmenu_method_call,
|
||||
.get_property = dbusmenu_get_property,
|
||||
.set_property = NULL,
|
||||
};
|
||||
|
||||
static void sni_method_call(G_GNUC_UNUSED GDBusConnection *connection,
|
||||
G_GNUC_UNUSED const gchar *sender,
|
||||
G_GNUC_UNUSED const gchar *object_path,
|
||||
G_GNUC_UNUSED const gchar *interface_name,
|
||||
const gchar *method_name,
|
||||
G_GNUC_UNUSED GVariant *parameters,
|
||||
GDBusMethodInvocation *invocation,
|
||||
gpointer user_data) {
|
||||
MoeMojiApplication *self = MOEMOJI_APPLICATION(user_data);
|
||||
|
||||
if (g_strcmp0(method_name, "Activate") == 0) {
|
||||
toggle_window(self);
|
||||
}
|
||||
|
||||
g_dbus_method_invocation_return_value(invocation, NULL);
|
||||
}
|
||||
|
||||
GVariant *sni_get_property(G_GNUC_UNUSED GDBusConnection *connection,
|
||||
G_GNUC_UNUSED const gchar *sender,
|
||||
G_GNUC_UNUSED const gchar *object_path,
|
||||
G_GNUC_UNUSED const gchar *interface_name,
|
||||
const gchar *property_name,
|
||||
G_GNUC_UNUSED GError **error, gpointer user_data) {
|
||||
if (g_strcmp0(property_name, "Category") == 0)
|
||||
return g_variant_new_string("ApplicationStatus");
|
||||
if (g_strcmp0(property_name, "Id") == 0)
|
||||
return g_variant_new_string("moemoji");
|
||||
if (g_strcmp0(property_name, "Title") == 0)
|
||||
return g_variant_new_string("MoeMoji");
|
||||
if (g_strcmp0(property_name, "Status") == 0)
|
||||
return g_variant_new_string("Active");
|
||||
if (g_strcmp0(property_name, "IconName") == 0) {
|
||||
MoeMojiApplication *self = MOEMOJI_APPLICATION(user_data);
|
||||
return g_variant_new_string(self->tray_icon_name
|
||||
? self->tray_icon_name
|
||||
: "net.angeltech.MoeMoji-tray-dark");
|
||||
}
|
||||
if (g_strcmp0(property_name, "ItemIsMenu") == 0)
|
||||
return g_variant_new_boolean(FALSE);
|
||||
if (g_strcmp0(property_name, "Menu") == 0)
|
||||
return g_variant_new_object_path("/MenuBar");
|
||||
if (g_strcmp0(property_name, "IconThemePath") == 0) {
|
||||
MoeMojiApplication *self = MOEMOJI_APPLICATION(user_data);
|
||||
return g_variant_new_string(self->icon_theme_path ? self->icon_theme_path
|
||||
: "");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const GDBusInterfaceVTable sni_vtable = {
|
||||
.method_call = sni_method_call,
|
||||
.get_property = sni_get_property,
|
||||
.set_property = NULL,
|
||||
};
|
||||
|
||||
static void on_sni_bus_name_acquired(GDBusConnection *connection,
|
||||
const gchar *name,
|
||||
G_GNUC_UNUSED gpointer user_data) {
|
||||
g_dbus_connection_call(
|
||||
connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher",
|
||||
"org.kde.StatusNotifierWatcher", "RegisterStatusNotifierItem",
|
||||
g_variant_new("(s)", name), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL,
|
||||
NULL);
|
||||
}
|
||||
|
||||
static void update_tray_icon(MoeMojiApplication *self) {
|
||||
AdwStyleManager *sm = adw_style_manager_get_default();
|
||||
gboolean dark = adw_style_manager_get_dark(sm);
|
||||
self->tray_icon_name = dark ? "net.angeltech.MoeMoji-tray-dark"
|
||||
: "net.angeltech.MoeMoji-tray-light";
|
||||
|
||||
if (self->dbus_conn && self->sni_registration_id > 0) {
|
||||
g_dbus_connection_emit_signal(self->dbus_conn, NULL, "/StatusNotifierItem",
|
||||
"org.kde.StatusNotifierItem", "NewIcon", NULL,
|
||||
NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_dark_changed(G_GNUC_UNUSED GObject *obj,
|
||||
G_GNUC_UNUSED GParamSpec *pspec,
|
||||
gpointer user_data) {
|
||||
update_tray_icon(MOEMOJI_APPLICATION(user_data));
|
||||
}
|
||||
|
||||
static void setup_sni(MoeMojiApplication *self) {
|
||||
GError *error = NULL;
|
||||
self->dbus_conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
|
||||
if (self->dbus_conn == NULL) {
|
||||
g_warning("session bus: %s", error->message);
|
||||
g_clear_error(&error);
|
||||
return;
|
||||
}
|
||||
GDBusNodeInfo *sni_node =
|
||||
g_dbus_node_info_new_for_xml(sni_introspection_xml, &error);
|
||||
if (sni_node == NULL) {
|
||||
g_warning("Failed to parse SNI introspection XML: %s", error->message);
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
self->sni_registration_id = g_dbus_connection_register_object(
|
||||
self->dbus_conn, "/StatusNotifierItem", sni_node->interfaces[0],
|
||||
&sni_vtable, self, NULL, &error);
|
||||
g_dbus_node_info_unref(sni_node);
|
||||
if (self->sni_registration_id == 0) {
|
||||
g_warning("Failed to register SNI object: %s", error->message);
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
GDBusNodeInfo *menu_node =
|
||||
g_dbus_node_info_new_for_xml(dbusmenu_introspection_xml, &error);
|
||||
if (menu_node == NULL) {
|
||||
g_warning("Failed to parse dbusmenu introspection XML: %s", error->message);
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
self->menu_registration_id = g_dbus_connection_register_object(
|
||||
self->dbus_conn, "/MenuBar", menu_node->interfaces[0], &dbusmenu_vtable,
|
||||
self, NULL, &error);
|
||||
g_dbus_node_info_unref(menu_node);
|
||||
if (self->menu_registration_id == 0) {
|
||||
g_warning("dbusmenu register: %s", error->message);
|
||||
g_error_free(error);
|
||||
}
|
||||
g_autofree gchar *bus_name =
|
||||
g_strdup_printf("org.kde.StatusNotifierItem-%d-1", getpid());
|
||||
self->sni_bus_name_id = g_bus_own_name_on_connection(
|
||||
self->dbus_conn, bus_name, G_BUS_NAME_OWNER_FLAGS_NONE,
|
||||
on_sni_bus_name_acquired, NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
static void on_shortcuts_activated(GDBusProxy *proxy, const gchar *sender_name,
|
||||
const gchar *signal_name,
|
||||
GVariant *parameters, gpointer user_data) {
|
||||
(void)proxy;
|
||||
(void)sender_name;
|
||||
if (g_strcmp0(signal_name, "Activated") != 0)
|
||||
return;
|
||||
MoeMojiApplication *self = MOEMOJI_APPLICATION(user_data);
|
||||
const gchar *shortcut_id = NULL;
|
||||
g_variant_get_child(parameters, 1, "&s", &shortcut_id);
|
||||
if (g_strcmp0(shortcut_id, "toggle-window") == 0)
|
||||
toggle_window(self);
|
||||
}
|
||||
|
||||
static void
|
||||
on_bind_shortcuts_response(G_GNUC_UNUSED GDBusConnection *connection,
|
||||
G_GNUC_UNUSED const gchar *sender_name,
|
||||
G_GNUC_UNUSED const gchar *object_path,
|
||||
G_GNUC_UNUSED const gchar *interface_name,
|
||||
G_GNUC_UNUSED const gchar *signal_name,
|
||||
GVariant *parameters,
|
||||
G_GNUC_UNUSED gpointer user_data) {
|
||||
guint32 response;
|
||||
g_autoptr(GVariant) results = NULL;
|
||||
g_variant_get(parameters, "(u@a{sv})", &response, &results);
|
||||
if (response != 0)
|
||||
g_warning("BindShortcuts portal response: %u", response);
|
||||
}
|
||||
|
||||
static void bind_shortcuts(MoeMojiApplication *self) {
|
||||
GVariantBuilder shortcut_builder;
|
||||
g_variant_builder_init(&shortcut_builder, G_VARIANT_TYPE("a(sa{sv})"));
|
||||
g_variant_builder_open(&shortcut_builder, G_VARIANT_TYPE("(sa{sv})"));
|
||||
g_variant_builder_add(&shortcut_builder, "s", "toggle-window");
|
||||
GVariantBuilder props_builder;
|
||||
g_variant_builder_init(&props_builder, G_VARIANT_TYPE("a{sv}"));
|
||||
g_variant_builder_add(&props_builder, "{sv}", "description",
|
||||
g_variant_new_string("Toggle MoeMoji"));
|
||||
g_variant_builder_add(&props_builder, "{sv}", "preferred_trigger",
|
||||
g_variant_new_string("SHIFT+LOGO+E"));
|
||||
g_variant_builder_add_value(&shortcut_builder,
|
||||
g_variant_builder_end(&props_builder));
|
||||
g_variant_builder_close(&shortcut_builder);
|
||||
GVariantBuilder options_builder;
|
||||
g_variant_builder_init(&options_builder, G_VARIANT_TYPE("a{sv}"));
|
||||
|
||||
g_autofree gchar *token = g_strdup_printf("moemoji_bind_%d", getpid());
|
||||
const gchar *unique = g_dbus_connection_get_unique_name(self->dbus_conn);
|
||||
g_autofree gchar *unique_escaped = g_strdup(unique + 1);
|
||||
for (gchar *p = unique_escaped; *p; p++) {
|
||||
if (*p == '.')
|
||||
*p = '_';
|
||||
}
|
||||
g_autofree gchar *request_path = g_strdup_printf(
|
||||
"/org/freedesktop/portal/desktop/request/%s/%s", unique_escaped, token);
|
||||
|
||||
g_variant_builder_add(&options_builder, "{sv}", "handle_token",
|
||||
g_variant_new_string(token));
|
||||
|
||||
g_dbus_connection_signal_subscribe(
|
||||
self->dbus_conn, "org.freedesktop.portal.Desktop",
|
||||
"org.freedesktop.portal.Request", "Response", request_path, NULL,
|
||||
G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, on_bind_shortcuts_response, self,
|
||||
NULL);
|
||||
|
||||
const gchar *parent = "";
|
||||
g_dbus_proxy_call(
|
||||
self->shortcuts_proxy, "BindShortcuts",
|
||||
g_variant_new("(o@a(sa{sv})s@a{sv})", self->shortcuts_session_path,
|
||||
g_variant_builder_end(&shortcut_builder), parent,
|
||||
g_variant_builder_end(&options_builder)),
|
||||
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
on_create_session_response(G_GNUC_UNUSED GDBusConnection *connection,
|
||||
G_GNUC_UNUSED const gchar *sender_name,
|
||||
G_GNUC_UNUSED const gchar *object_path,
|
||||
G_GNUC_UNUSED const gchar *interface_name,
|
||||
G_GNUC_UNUSED const gchar *signal_name,
|
||||
GVariant *parameters, gpointer user_data) {
|
||||
MoeMojiApplication *self = MOEMOJI_APPLICATION(user_data);
|
||||
guint32 response;
|
||||
g_autoptr(GVariant) results = NULL;
|
||||
g_variant_get(parameters, "(u@a{sv})", &response, &results);
|
||||
if (response != 0) {
|
||||
g_warning("GlobalShortcuts CreateSession failed with response %u",
|
||||
response);
|
||||
return;
|
||||
}
|
||||
const gchar *session_handle = NULL;
|
||||
g_variant_lookup(results, "session_handle", "&s", &session_handle);
|
||||
if (session_handle == NULL) {
|
||||
g_warning("GlobalShortcuts: No session_handle in CreateSession response");
|
||||
return;
|
||||
}
|
||||
self->shortcuts_session_path = g_strdup(session_handle);
|
||||
g_signal_connect(self->shortcuts_proxy, "g-signal",
|
||||
G_CALLBACK(on_shortcuts_activated), self);
|
||||
bind_shortcuts(self);
|
||||
}
|
||||
|
||||
static gboolean begin_create_session(MoeMojiApplication *self) {
|
||||
const gchar *session_token = "moemoji_session";
|
||||
g_autofree gchar *handle_token =
|
||||
g_strdup_printf("moemoji_handle_%d", getpid());
|
||||
const gchar *unique = g_dbus_connection_get_unique_name(self->dbus_conn);
|
||||
g_autofree gchar *unique_escaped = g_strdup(unique + 1);
|
||||
for (gchar *p = unique_escaped; *p; p++) {
|
||||
if (*p == '.')
|
||||
*p = '_';
|
||||
}
|
||||
g_autofree gchar *request_path =
|
||||
g_strdup_printf("/org/freedesktop/portal/desktop/request/%s/%s",
|
||||
unique_escaped, handle_token);
|
||||
|
||||
g_dbus_connection_signal_subscribe(
|
||||
self->dbus_conn, "org.freedesktop.portal.Desktop",
|
||||
"org.freedesktop.portal.Request", "Response", request_path, NULL,
|
||||
G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, on_create_session_response, self,
|
||||
NULL);
|
||||
GVariantBuilder options;
|
||||
g_variant_builder_init(&options, G_VARIANT_TYPE("a{sv}"));
|
||||
g_variant_builder_add(&options, "{sv}", "session_handle_token",
|
||||
g_variant_new_string(session_token));
|
||||
g_variant_builder_add(&options, "{sv}", "handle_token",
|
||||
g_variant_new_string(handle_token));
|
||||
|
||||
g_dbus_proxy_call(self->shortcuts_proxy, "CreateSession",
|
||||
g_variant_new("(@a{sv})", g_variant_builder_end(&options)),
|
||||
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void setup_global_shortcuts(MoeMojiApplication *self) {
|
||||
GError *error = NULL;
|
||||
if (self->dbus_conn == NULL)
|
||||
return;
|
||||
if (self->shortcuts_bound)
|
||||
return;
|
||||
self->shortcuts_bound = TRUE;
|
||||
self->shortcuts_proxy = g_dbus_proxy_new_sync(
|
||||
self->dbus_conn,
|
||||
G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
|
||||
G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
|
||||
NULL, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.GlobalShortcuts", NULL, &error);
|
||||
if (self->shortcuts_proxy == NULL) {
|
||||
g_warning("GlobalShortcuts: Failed to create proxy: %s", error->message);
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
g_timeout_add(500, (GSourceFunc)begin_create_session, self);
|
||||
}
|
||||
|
||||
MoeMojiApplication *moemoji_application_new(gchar *application_id,
|
||||
GApplicationFlags flags) {
|
||||
return g_object_new(MOEMOJI_TYPE_APPLICATION, "application-id",
|
||||
application_id, "flags", flags, NULL);
|
||||
}
|
||||
|
||||
static void moemoji_application_startup(GApplication *app) {
|
||||
G_APPLICATION_CLASS(moemoji_application_parent_class)->startup(app);
|
||||
MoeMojiApplication *self = MOEMOJI_APPLICATION(app);
|
||||
gtk_icon_theme_add_resource_path(
|
||||
gtk_icon_theme_get_for_display(gdk_display_get_default()),
|
||||
"/net/angeltech/MoeMoji/icons");
|
||||
gtk_window_set_default_icon_name("net.angeltech.MoeMoji-symbolic");
|
||||
gboolean in_flatpak = g_file_test("/.flatpak-info", G_FILE_TEST_EXISTS);
|
||||
if (in_flatpak) {
|
||||
self->icon_theme_path = NULL;
|
||||
} else {
|
||||
const char *src_dir = g_getenv("MESON_SOURCE_ROOT");
|
||||
if (src_dir) {
|
||||
self->icon_theme_path = g_build_filename(src_dir, "data", "icons", NULL);
|
||||
} else {
|
||||
g_autofree char *cwd_icons =
|
||||
g_build_filename("data", "icons", "hicolor", NULL);
|
||||
if (g_file_test(cwd_icons, G_FILE_TEST_IS_DIR)) {
|
||||
g_autofree char *cwd = g_get_current_dir();
|
||||
self->icon_theme_path = g_build_filename(cwd, "data", "icons", NULL);
|
||||
} else {
|
||||
self->icon_theme_path =
|
||||
g_build_filename(MOEMOJI_DATADIR, "..", "icons", NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
setup_sni(self);
|
||||
update_tray_icon(self);
|
||||
g_signal_connect(adw_style_manager_get_default(), "notify::dark",
|
||||
G_CALLBACK(on_dark_changed), self);
|
||||
setup_global_shortcuts(self);
|
||||
}
|
||||
|
||||
static void moemoji_application_activate(GApplication *app) {
|
||||
g_assert(MOEMOJI_IS_APPLICATION(app));
|
||||
MoeMojiApplication *self = MOEMOJI_APPLICATION(app);
|
||||
if (!self->window_created) {
|
||||
self->window = g_object_new(MOEMOJI_TYPE_WINDOW, "application",
|
||||
GTK_APPLICATION(self), NULL);
|
||||
g_signal_connect(self->window, "close-request",
|
||||
G_CALLBACK(on_close_request), NULL);
|
||||
self->window_created = TRUE;
|
||||
} else {
|
||||
toggle_window(self);
|
||||
}
|
||||
}
|
||||
|
||||
static void moemoji_application_dispose(GObject *object) {
|
||||
MoeMojiApplication *self = MOEMOJI_APPLICATION(object);
|
||||
if (self->sni_registration_id > 0 && self->dbus_conn != NULL) {
|
||||
g_dbus_connection_unregister_object(self->dbus_conn,
|
||||
self->sni_registration_id);
|
||||
self->sni_registration_id = 0;
|
||||
}
|
||||
if (self->menu_registration_id > 0 && self->dbus_conn != NULL) {
|
||||
g_dbus_connection_unregister_object(self->dbus_conn,
|
||||
self->menu_registration_id);
|
||||
self->menu_registration_id = 0;
|
||||
}
|
||||
if (self->sni_bus_name_id > 0) {
|
||||
g_bus_unown_name(self->sni_bus_name_id);
|
||||
self->sni_bus_name_id = 0;
|
||||
}
|
||||
g_clear_object(&self->shortcuts_proxy);
|
||||
g_clear_pointer(&self->shortcuts_session_path, g_free);
|
||||
g_clear_pointer(&self->icon_theme_path, g_free);
|
||||
g_clear_object(&self->dbus_conn);
|
||||
G_OBJECT_CLASS(moemoji_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void moemoji_application_show_about(G_GNUC_UNUSED GSimpleAction *action,
|
||||
G_GNUC_UNUSED GVariant *parameter,
|
||||
gpointer user_data) {
|
||||
MoeMojiApplication *self = MOEMOJI_APPLICATION(user_data);
|
||||
GtkWindow *window = NULL;
|
||||
g_return_if_fail(MOEMOJI_IS_APPLICATION(self));
|
||||
window = gtk_application_get_active_window(GTK_APPLICATION(self));
|
||||
gtk_show_about_dialog(window, "program-name", "MoeMoji", "version", "0.1.0",
|
||||
NULL);
|
||||
}
|
||||
|
||||
static void moemoji_application_class_init(MoeMojiApplicationClass *klass) {
|
||||
GApplicationClass *app_class = G_APPLICATION_CLASS(klass);
|
||||
GObjectClass *object_class = G_OBJECT_CLASS(klass);
|
||||
app_class->startup = moemoji_application_startup;
|
||||
app_class->activate = moemoji_application_activate;
|
||||
object_class->dispose = moemoji_application_dispose;
|
||||
}
|
||||
|
||||
static void moemoji_application_init(MoeMojiApplication *self) {
|
||||
self->settings = g_settings_new("net.angeltech.MoeMoji");
|
||||
self->window_created = FALSE;
|
||||
self->shortcuts_bound = FALSE;
|
||||
g_autoptr(GSimpleAction) quit_action = g_simple_action_new("quit", NULL);
|
||||
g_signal_connect_swapped(quit_action, "activate",
|
||||
G_CALLBACK(g_application_quit), self);
|
||||
g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(quit_action));
|
||||
g_autoptr(GSimpleAction) about_action = g_simple_action_new("about", NULL);
|
||||
g_signal_connect(about_action, "activate",
|
||||
G_CALLBACK(moemoji_application_show_about), self);
|
||||
g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(about_action));
|
||||
gtk_application_set_accels_for_action(GTK_APPLICATION(self), "app.quit",
|
||||
(const char *[]){
|
||||
"<primary>q",
|
||||
NULL,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "moemoji-window.h"
|
||||
#include <adwaita.h>
|
||||
|
||||
struct _MoeMojiApplication {
|
||||
AdwApplication parent_instance;
|
||||
GSettings *settings;
|
||||
MoeMojiWindow *window;
|
||||
gboolean window_created;
|
||||
gboolean shortcuts_bound;
|
||||
GDBusConnection *dbus_conn;
|
||||
guint sni_registration_id;
|
||||
guint sni_bus_name_id;
|
||||
guint menu_registration_id;
|
||||
gchar *icon_theme_path;
|
||||
const gchar *tray_icon_name;
|
||||
GDBusProxy *shortcuts_proxy;
|
||||
gchar *shortcuts_session_path;
|
||||
};
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define MOEMOJI_TYPE_APPLICATION (moemoji_application_get_type())
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MoeMojiApplication, moemoji_application, MOEMOJI,
|
||||
APPLICATION, AdwApplication)
|
||||
|
||||
MoeMojiApplication *moemoji_application_new(gchar *application_id,
|
||||
GApplicationFlags flags);
|
||||
|
||||
G_END_DECLS
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
char *make_display_name(const char *dirname);
|
||||
char *find_kaomoji_dir(void);
|
||||
|
||||
GVariant *sni_get_property(GDBusConnection *connection, const gchar *sender,
|
||||
const gchar *object_path,
|
||||
const gchar *interface_name,
|
||||
const gchar *property_name, GError **error,
|
||||
gpointer user_data);
|
||||
|
||||
GVariant *dbusmenu_get_property(GDBusConnection *connection,
|
||||
const gchar *sender, const gchar *object_path,
|
||||
const gchar *interface_name,
|
||||
const gchar *property_name, GError **error,
|
||||
gpointer user_data);
|
||||
@@ -0,0 +1,279 @@
|
||||
#include "moemoji-window.h"
|
||||
#include "moemoji-config.h"
|
||||
#include "moemoji-internal.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
G_DEFINE_TYPE(MoeMojiWindow, moemoji_window, GTK_TYPE_APPLICATION_WINDOW)
|
||||
|
||||
static void category_widgets_free(gpointer data) {
|
||||
CategoryWidgets *cw = data;
|
||||
g_free(cw->name);
|
||||
g_free(cw);
|
||||
}
|
||||
|
||||
char *make_display_name(const char *dirname) {
|
||||
char *name = g_strdup(dirname);
|
||||
for (char *p = name; *p; p++) {
|
||||
if (*p == '_')
|
||||
*p = ' ';
|
||||
}
|
||||
if (name[0])
|
||||
name[0] = g_ascii_toupper(name[0]);
|
||||
return name;
|
||||
}
|
||||
|
||||
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,
|
||||
G_GNUC_UNUSED gpointer 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);
|
||||
}
|
||||
|
||||
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 add_kaomoji_button(GtkFlowBox *flow, const char *text) {
|
||||
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_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);
|
||||
|
||||
g_signal_connect(button, "clicked", G_CALLBACK(on_kaomoji_clicked), NULL);
|
||||
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);
|
||||
|
||||
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 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 = make_display_name(dirname);
|
||||
GtkWidget *header = gtk_label_new(display_name);
|
||||
gtk_widget_add_css_class(header, "category-header");
|
||||
gtk_label_set_xalign(GTK_LABEL(header), 0.0);
|
||||
gtk_box_append(self->content_box, header);
|
||||
GtkWidget *flow = gtk_flow_box_new();
|
||||
gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(flow), FALSE);
|
||||
gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(flow), 2);
|
||||
gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(flow), 10);
|
||||
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(flow), GTK_SELECTION_NONE);
|
||||
const char *filename;
|
||||
while ((filename = g_dir_read_name(dir)) != NULL) {
|
||||
if (!g_str_has_suffix(filename, ".txt"))
|
||||
continue;
|
||||
char *filepath = g_build_filename(cat_path, filename, NULL);
|
||||
char *contents = NULL;
|
||||
if (g_file_get_contents(filepath, &contents, NULL, NULL)) {
|
||||
g_strchomp(contents);
|
||||
if (contents[0] != '\0')
|
||||
add_kaomoji_button(GTK_FLOW_BOX(flow), contents);
|
||||
g_free(contents);
|
||||
}
|
||||
g_free(filepath);
|
||||
}
|
||||
gtk_box_append(self->content_box, flow);
|
||||
CategoryWidgets *cw = g_new0(CategoryWidgets, 1);
|
||||
cw->header = header;
|
||||
cw->flow = flow;
|
||||
cw->name = display_name;
|
||||
g_ptr_array_add(self->category_widgets, cw);
|
||||
g_dir_close(dir);
|
||||
g_free(cat_path);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
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);
|
||||
}
|
||||
gtk_widget_set_visible(cw->header, visible);
|
||||
gtk_widget_set_visible(cw->flow, visible);
|
||||
}
|
||||
}
|
||||
|
||||
static void moemoji_window_class_init(MoeMojiWindowClass *klass) {
|
||||
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
|
||||
gtk_widget_class_set_template_from_resource(
|
||||
widget_class, "/net/angeltech/MoeMoji/moemoji-window.ui");
|
||||
gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, outer_box);
|
||||
gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow,
|
||||
content_box);
|
||||
gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow,
|
||||
search_entry);
|
||||
gtk_widget_class_bind_template_child(widget_class, MoeMojiWindow, header_bar);
|
||||
}
|
||||
|
||||
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 gint compare_entries(gconstpointer a, gconstpointer b) {
|
||||
char **ea = *(char ***)a;
|
||||
char **eb = *(char ***)b;
|
||||
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);
|
||||
return strcmp(ka, kb);
|
||||
}
|
||||
|
||||
static void moemoji_window_init(MoeMojiWindow *self) {
|
||||
gtk_widget_init_template(GTK_WIDGET(self));
|
||||
gtk_widget_add_css_class(GTK_WIDGET(self), "wallpaper-bg");
|
||||
gtk_widget_add_css_class(GTK_WIDGET(self->header_bar), "wallpaper-bg");
|
||||
gtk_widget_add_css_class(GTK_WIDGET(self->content_box), "content-area");
|
||||
self->category_widgets =
|
||||
g_ptr_array_new_with_free_func(category_widgets_free);
|
||||
char *kaomoji_dir = find_kaomoji_dir();
|
||||
g_autofree char *user_dir =
|
||||
g_build_filename(g_get_user_data_dir(), "moemoji", "kaomoji", NULL);
|
||||
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);
|
||||
if (entries->len == 0) {
|
||||
GtkWidget *label = gtk_label_new("No kaomoji data found.");
|
||||
gtk_box_append(self->content_box, label);
|
||||
g_ptr_array_free(entries, TRUE);
|
||||
g_free(kaomoji_dir);
|
||||
return;
|
||||
}
|
||||
g_ptr_array_sort(entries, compare_entries);
|
||||
for (guint i = 0; i < entries->len; i++) {
|
||||
char **pair = g_ptr_array_index(entries, i);
|
||||
load_category(self, pair[0], pair[1]);
|
||||
g_free(pair[0]);
|
||||
g_free(pair[1]);
|
||||
g_free(pair);
|
||||
}
|
||||
g_ptr_array_free(entries, TRUE);
|
||||
g_free(kaomoji_dir);
|
||||
g_signal_connect(self->search_entry, "search-changed",
|
||||
G_CALLBACK(on_search_changed), self);
|
||||
if (self->category_widgets->len > 0) {
|
||||
GtkWidget *flow = gtk_flow_box_new();
|
||||
gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(flow), FALSE);
|
||||
gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(flow), 20);
|
||||
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(flow), GTK_SELECTION_NONE);
|
||||
gtk_widget_add_css_class(flow, "category-chips");
|
||||
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");
|
||||
g_signal_connect(btn, "clicked", G_CALLBACK(on_chip_clicked), self);
|
||||
gtk_flow_box_insert(GTK_FLOW_BOX(flow), btn, -1);
|
||||
}
|
||||
self->category_bar = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
|
||||
gtk_widget_add_css_class(GTK_WIDGET(self->category_bar), "category-bar");
|
||||
gtk_widget_set_size_request(GTK_WIDGET(self->category_bar), -1, 80);
|
||||
GtkWidget *cat_scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(cat_scroll),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
|
||||
gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(cat_scroll),
|
||||
80);
|
||||
gtk_scrolled_window_set_propagate_natural_height(
|
||||
GTK_SCROLLED_WINDOW(cat_scroll), TRUE);
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
typedef struct {
|
||||
GtkWidget *header;
|
||||
GtkWidget *flow;
|
||||
char *name;
|
||||
} CategoryWidgets;
|
||||
|
||||
struct _MoeMojiWindow {
|
||||
GtkApplicationWindow parent_instance;
|
||||
GtkBox *outer_box;
|
||||
GtkBox *content_box;
|
||||
GtkSearchEntry *search_entry;
|
||||
GtkWidget *header_bar;
|
||||
GtkBox *category_bar;
|
||||
GPtrArray *category_widgets;
|
||||
};
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define MOEMOJI_TYPE_WINDOW (moemoji_window_get_type())
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MoeMojiWindow, moemoji_window, MOEMOJI, WINDOW,
|
||||
GtkApplicationWindow);
|
||||
|
||||
G_END_DECLS
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-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="resizable">True</property>
|
||||
<property name="title">MoeMoji</property>
|
||||
|
||||
<child type="titlebar">
|
||||
<object class="AdwHeaderBar" id="header_bar">
|
||||
<property name="show-end-title-buttons">True</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkBox" id="outer_box">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="search_entry">
|
||||
<property name="placeholder-text">Filter by 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">
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<property name="vexpand">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="content_box">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="margin-start">6</property>
|
||||
<property name="margin-end">6</property>
|
||||
<property name="margin-top">6</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
<property name="spacing">4</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/net/angeltech/MoeMoji">
|
||||
<file>moemoji-window.ui</file>
|
||||
<file>style.css</file>
|
||||
<file>wp.png</file>
|
||||
<file alias="icons/scalable/apps/net.angeltech.MoeMoji.svg">../data/icons/hicolor/scalable/apps/net.angeltech.MoeMoji.svg</file>
|
||||
<file alias="icons/scalable/apps/net.angeltech.MoeMoji-symbolic.svg">../data/icons/hicolor/symbolic/apps/net.angeltech.MoeMoji-symbolic.svg</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
@@ -0,0 +1,55 @@
|
||||
.kaomoji-button {
|
||||
font-size: 14px;
|
||||
min-width: 60px;
|
||||
min-height: 36px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.kaomoji-wide {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding: 8px 4px 4px 4px;
|
||||
}
|
||||
|
||||
.kaomoji-preview {
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.category-chips {
|
||||
margin: 4px 6px;
|
||||
}
|
||||
|
||||
.category-chip {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.wallpaper-bg {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/net/angeltech/MoeMoji/wp.png");
|
||||
background-size: cover;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
wallpaper-bg > * {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#header_bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#header_bar > * {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.category-bar {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 14px;
|
||||
padding: 8px;
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
Reference in New Issue
Block a user