Embedd assets icons
This commit is contained in:
155
src/main.cpp
155
src/main.cpp
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user