Embedd assets icons

This commit is contained in:
2026-10-18 02:57:29 +02:00
parent b77b4a3fc6
commit 726a7ae92d
19 changed files with 1202 additions and 126 deletions

View File

@@ -10,6 +10,8 @@
#include <vector>
#include "capturedialog.h"
#include "daemonclient.h"
#include "ipc_ids.h"
#include "keyrow.h"
#include "mapping.h"
#include "mappings.h"
@@ -29,6 +31,59 @@ using gkeymapper::kNumKeys;
std::vector<KeyRow> rows;
// Footer view of the daemon. It is filled by the asynchronous callbacks of
// DaemonClient, which arrive on the GTK main loop, so nothing here ever waits
// for the daemon.
struct DaemonFooter {
bool reachable = false;
std::string device;
std::string lastKey;
} daemonFooter;
GtkWidget* daemonLabel = nullptr;
gkeymapper::DaemonClient daemonClient;
// Rebuilds the footer line from the state above. " · " is a separator, not a
// sentence, so it is not translated.
static void refresh_footer() {
if (daemonLabel == nullptr) {
return;
}
std::string text;
if (!daemonFooter.reachable) {
text = _("Daemon: not reachable");
} else {
text = _("Daemon: running");
if (!daemonFooter.device.empty()) {
g_autofree gchar* device =
g_strdup_printf(_("Device: %s"), daemonFooter.device.c_str());
text += " · ";
text += device;
}
if (!daemonFooter.lastKey.empty()) {
text += " · ";
text += daemonFooter.lastKey;
}
}
gtk_label_set_text(GTK_LABEL(daemonLabel), text.c_str());
}
static void on_daemon_status(const gkeymapper::DaemonStatus& status) {
daemonFooter.reachable = status.reachable;
daemonFooter.device = status.device;
if (!status.reachable) {
daemonFooter.lastKey.clear(); // stale once the daemon is gone
}
refresh_footer();
}
static void on_daemon_key(int index, const std::string& action) {
g_autofree gchar* text =
g_strdup_printf(_("Last key: G%d → %s"), index + 1, action.c_str());
daemonFooter.lastKey = text;
refresh_footer();
}
static void on_pick_file_response(GtkDialog* dialog, int response, gpointer userData) {
if (response == GTK_RESPONSE_ACCEPT) {
KeyRow* row = static_cast<KeyRow*>(userData);
@@ -101,6 +156,10 @@ void on_save_clicked(GtkButton* /*btn*/, gpointer /*userData*/) {
return;
}
// Best effort: the daemon would pick the save up through its file watch
// within about half a second, this just applies it right away.
daemonClient.reload();
std::cout << _("Mappings saved to ") << path << "\n";
}
@@ -134,24 +193,50 @@ static void on_import_response(GtkDialog* dialog, int response, gpointer /*userD
static std::string find_asset(const std::string& name);
// The icons are compiled into the executable (data/icons.gresource.xml), so the
// GUI has them even when no assets are installed. The file lookup stays as a
// fallback for a build without the resource; because the resource is embedded,
// it wins over any installed file.
static const char* kIconResource = "/dev/rager/gkeymapper/icon.png";
static const char* kIconThemeResource = "/dev/rager/gkeymapper/icons";
static bool have_embedded_icon() {
return g_resources_get_info(kIconResource, G_RESOURCE_LOOKUP_FLAGS_NONE, nullptr, nullptr,
nullptr);
}
static GtkWidget* make_icon_image(int pixelSize) {
GtkWidget* image = have_embedded_icon()
? gtk_image_new_from_resource(kIconResource)
: gtk_image_new_from_file(find_asset("icon.png").c_str());
gtk_image_set_pixel_size(GTK_IMAGE(image), pixelSize);
return image;
}
static void show_about_dialog(GtkWindow* parent) {
GtkWidget* dialog = gtk_about_dialog_new();
gtk_about_dialog_set_program_name(GTK_ABOUT_DIALOG(dialog), _("gKeyMapper"));
gtk_about_dialog_set_version(GTK_ABOUT_DIALOG(dialog), "0.1.0");
gtk_about_dialog_set_version(GTK_ABOUT_DIALOG(dialog), gkeymapper::ipc::kVersion);
gtk_about_dialog_set_comments(GTK_ABOUT_DIALOG(dialog),
_("G-Key mapper for the Logitech G910 (GTK4)."));
gtk_about_dialog_set_website(GTK_ABOUT_DIALOG(dialog), "https://rager.dev");
gtk_about_dialog_set_website_label(GTK_ABOUT_DIALOG(dialog), "Thomas Rager - rager.dev");
std::string iconPath = find_asset("icon.png");
GError* error = nullptr;
GdkTexture* texture = gdk_texture_new_from_filename(iconPath.c_str(), &error);
GdkTexture* texture = nullptr;
if (have_embedded_icon()) {
texture = gdk_texture_new_from_resource(kIconResource);
} else {
std::string iconPath = find_asset("icon.png");
GError* error = nullptr;
texture = gdk_texture_new_from_filename(iconPath.c_str(), &error);
if (!texture && error) {
g_printerr("Could not load icon: %s\n", error->message);
g_error_free(error);
}
}
if (texture) {
gtk_about_dialog_set_logo(GTK_ABOUT_DIALOG(dialog), GDK_PAINTABLE(texture));
g_object_unref(texture);
} else if (error) {
g_printerr("Could not load icon: %s\n", error->message);
g_error_free(error);
}
gtk_window_set_transient_for(GTK_WINDOW(dialog), parent);
g_signal_connect(dialog, "response", G_CALLBACK(gtk_window_destroy), NULL);
@@ -236,10 +321,14 @@ void build_ui(GtkApplication* app) {
GtkWidget* headerBar = gtk_header_bar_new();
gtk_window_set_titlebar(GTK_WINDOW(window), headerBar);
// Serve "gkeymapper" from the embedded icons, so the window icon resolves
// even without an installed hicolor theme.
gtk_icon_theme_add_resource_path(
gtk_icon_theme_get_for_display(gtk_widget_get_display(window)), kIconThemeResource);
gtk_window_set_icon_name(GTK_WINDOW(window), "gkeymapper");
GtkWidget* titleBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
GtkWidget* iconImage = gtk_image_new_from_file(find_asset("icon.png").c_str());
GtkWidget* iconImage = make_icon_image(24);
gtk_image_set_pixel_size(GTK_IMAGE(iconImage), 24);
gtk_box_append(GTK_BOX(titleBox), iconImage);
gtk_box_append(GTK_BOX(titleBox), gtk_label_new(_("gKeyMapper")));
@@ -261,23 +350,17 @@ void build_ui(GtkApplication* app) {
GtkWidget* outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_window_set_child(GTK_WINDOW(window), outer);
// Title / subtitle
GtkWidget* headerBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
gtk_widget_set_margin_top(headerBox, 16);
gtk_widget_set_margin_bottom(headerBox, 8);
gtk_widget_set_margin_start(headerBox, 16);
gtk_widget_set_margin_end(headerBox, 16);
gtk_box_append(GTK_BOX(outer), headerBox);
GtkWidget* title = gtk_label_new(nullptr);
gtk_label_set_markup(GTK_LABEL(title), "<span size='x-large' weight='bold'>G-Key Mapper</span>");
gtk_label_set_xalign(GTK_LABEL(title), 0.0f);
gtk_box_append(GTK_BOX(headerBox), title);
GtkWidget* subtitle = gtk_label_new(_("G1 to G9 of the Logitech G910 remapped to keys or commands"));
// Subtitle only: the app name is already in the header bar, and the heading
// spelled it differently ("G-Key Mapper") on top of that.
GtkWidget* subtitle =
gtk_label_new(_("G1 to G9 of the Logitech G910 remapped to keys or commands"));
gtk_label_set_xalign(GTK_LABEL(subtitle), 0.0f);
gtk_widget_add_css_class(subtitle, "dim-label");
gtk_box_append(GTK_BOX(headerBox), subtitle);
gtk_widget_set_margin_top(subtitle, 16);
gtk_widget_set_margin_bottom(subtitle, 8);
gtk_widget_set_margin_start(subtitle, 16);
gtk_widget_set_margin_end(subtitle, 16);
gtk_box_append(GTK_BOX(outer), subtitle);
// Table with all nine G keys (no scrolling needed, everything stays visible)
GtkWidget* grid = gtk_grid_new();
@@ -340,28 +423,44 @@ void build_ui(GtkApplication* app) {
update_row_state(&rows[i]);
}
// Footer with save button
GtkWidget* footer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
// Footer with save button and the daemon status line
GtkWidget* footer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
gtk_widget_set_margin_top(footer, 8);
gtk_widget_set_margin_bottom(footer, 16);
gtk_widget_set_margin_start(footer, 16);
gtk_widget_set_margin_end(footer, 16);
gtk_box_append(GTK_BOX(outer), footer);
GtkWidget* footerRow = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_box_append(GTK_BOX(footer), footerRow);
GtkWidget* saveBtn = gtk_button_new_with_label(_("Save mappings"));
// No "suggested-action" class on purpose: it gets a strong accent color
// (e.g. Mint-Y green) that clashes with the rest of the layout. The plain
// button follows the system theme like everything else.
g_signal_connect(saveBtn, "clicked", G_CALLBACK(on_save_clicked), nullptr);
gtk_box_append(GTK_BOX(footer), saveBtn);
gtk_box_append(GTK_BOX(footerRow), saveBtn);
GtkWidget* hint = gtk_label_new(_("Saved to ~/.config/gKeyMapper/mappings.conf"));
gtk_widget_add_css_class(hint, "dim-label");
gtk_widget_set_halign(hint, GTK_ALIGN_END);
gtk_widget_set_hexpand(hint, TRUE);
gtk_box_append(GTK_BOX(footer), hint);
gtk_box_append(GTK_BOX(footerRow), hint);
// Daemon status: control plane only, the keypress path never leaves the
// daemon. Text starts neutral so the footer does not change height when the
// first reply arrives. Plain label on purpose: GTK4's GtkStatusbar would add
// a message stack (pop/push) and is exposed over AT-SPI as a generic filler,
// so it brings no benefit here.
daemonLabel = gtk_label_new(_("Daemon: checking..."));
gtk_widget_add_css_class(daemonLabel, "dim-label");
gtk_label_set_xalign(GTK_LABEL(daemonLabel), 0.0f);
gtk_label_set_ellipsize(GTK_LABEL(daemonLabel), PANGO_ELLIPSIZE_END);
gtk_box_append(GTK_BOX(footer), daemonLabel);
gtk_window_present(GTK_WINDOW(window));
daemonClient.init(on_daemon_status, on_daemon_key);
}
static void on_activate(GtkApplication* app, gpointer /*user_data*/) {
@@ -392,7 +491,7 @@ int main(int argc, char** argv) {
bind_textdomain_codeset(PACKAGE, "UTF-8");
textdomain(PACKAGE);
GtkApplication* app = gtk_application_new("com.example.gkeymapper",
GtkApplication* app = gtk_application_new(gkeymapper::ipc::kAppId,
G_APPLICATION_FLAGS_NONE);
g_signal_connect(app, "activate", G_CALLBACK(on_activate), nullptr);
int status = g_application_run(G_APPLICATION(app), argc, argv);