2026-10-17 02:57:21 +02:00
|
|
|
|
// gKeyMapper - GTK4 GUI for remapping the G keys (G1-G9) of the Logitech G910.
|
2026-10-16 22:11:52 +02:00
|
|
|
|
// GUI only for now. Mappings are stored as a simple text file.
|
|
|
|
|
|
|
|
|
|
|
|
#include <gtk/gtk.h>
|
|
|
|
|
|
|
|
|
|
|
|
#include <iostream>
|
|
|
|
|
|
#include <locale.h>
|
2026-10-17 02:57:21 +02:00
|
|
|
|
#include <glib/gi18n.h>
|
2026-10-16 22:11:52 +02:00
|
|
|
|
#include <string>
|
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
2026-10-17 02:57:21 +02:00
|
|
|
|
#include "capturedialog.h"
|
|
|
|
|
|
#include "keyrow.h"
|
|
|
|
|
|
#include "mappings.h"
|
|
|
|
|
|
|
2026-10-16 22:11:52 +02:00
|
|
|
|
#ifndef PACKAGE
|
|
|
|
|
|
#define PACKAGE "gkeymapper"
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
#ifndef LOCALEDIR
|
|
|
|
|
|
#define LOCALEDIR "/usr/local/share/locale"
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
2026-10-17 02:57:21 +02:00
|
|
|
|
using gkeymapper::KeyRow;
|
|
|
|
|
|
using gkeymapper::kNumKeys;
|
2026-10-16 22:11:52 +02:00
|
|
|
|
|
|
|
|
|
|
std::vector<KeyRow> rows;
|
|
|
|
|
|
|
|
|
|
|
|
static void on_pick_file_response(GtkDialog* dialog, int response, gpointer userData) {
|
|
|
|
|
|
if (response == GTK_RESPONSE_ACCEPT) {
|
|
|
|
|
|
KeyRow* row = static_cast<KeyRow*>(userData);
|
|
|
|
|
|
GtkFileChooser* chooser = GTK_FILE_CHOOSER(dialog);
|
|
|
|
|
|
g_autoptr(GFile) file = gtk_file_chooser_get_file(chooser);
|
|
|
|
|
|
char* path = g_file_get_path(file);
|
|
|
|
|
|
if (path) {
|
|
|
|
|
|
gtk_editable_set_text(GTK_EDITABLE(row->targetEntry), path);
|
|
|
|
|
|
g_free(path);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
gtk_window_destroy(GTK_WINDOW(dialog));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void pick_command_file(KeyRow* row, GtkWindow* parent) {
|
|
|
|
|
|
GtkWidget* dialog = gtk_file_chooser_dialog_new(_("Choose file"),
|
|
|
|
|
|
parent,
|
|
|
|
|
|
GTK_FILE_CHOOSER_ACTION_OPEN,
|
|
|
|
|
|
_("_Cancel"), GTK_RESPONSE_CANCEL,
|
|
|
|
|
|
_("_Open"), GTK_RESPONSE_ACCEPT,
|
|
|
|
|
|
NULL);
|
|
|
|
|
|
g_signal_connect(dialog, "response", G_CALLBACK(on_pick_file_response), row);
|
|
|
|
|
|
gtk_window_present(GTK_WINDOW(dialog));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void on_capture_clicked(GtkButton* btn, gpointer userData) {
|
|
|
|
|
|
auto* row = static_cast<KeyRow*>(userData);
|
|
|
|
|
|
guint sel = gtk_drop_down_get_selected(GTK_DROP_DOWN(row->typeDrop));
|
|
|
|
|
|
|
2026-10-17 02:42:09 +02:00
|
|
|
|
GtkWindow* window = GTK_WINDOW(gtk_widget_get_root(GTK_WIDGET(btn)));
|
2026-10-16 22:11:52 +02:00
|
|
|
|
if (sel == 1) {
|
|
|
|
|
|
pick_command_file(row, window);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (sel == 0) {
|
2026-10-17 02:57:21 +02:00
|
|
|
|
gkeymapper::open_key_capture_dialog(row, window);
|
2026-10-16 22:11:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void update_row_state(KeyRow* row) {
|
|
|
|
|
|
guint sel = gtk_drop_down_get_selected(GTK_DROP_DOWN(row->typeDrop));
|
|
|
|
|
|
gboolean enabled = (sel != 2);
|
|
|
|
|
|
gtk_widget_set_sensitive(row->targetEntry, enabled);
|
|
|
|
|
|
gtk_widget_set_sensitive(row->captureButton, enabled);
|
|
|
|
|
|
|
|
|
|
|
|
if (sel == 1) {
|
|
|
|
|
|
gtk_button_set_label(GTK_BUTTON(row->captureButton), _("Choose file..."));
|
2026-10-17 02:42:09 +02:00
|
|
|
|
gtk_entry_set_placeholder_text(GTK_ENTRY(row->targetEntry),
|
|
|
|
|
|
_("e.g. /usr/bin/firefox or custom command"));
|
2026-10-16 22:11:52 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
gtk_button_set_label(GTK_BUTTON(row->captureButton), _("Capture"));
|
2026-10-17 02:42:09 +02:00
|
|
|
|
gtk_entry_set_placeholder_text(GTK_ENTRY(row->targetEntry),
|
|
|
|
|
|
_("e.g. ctrl+c, ctrl+v"));
|
2026-10-16 22:11:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
gtk_widget_set_size_request(row->captureButton, 180, -1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void on_type_changed(GObject* /*object*/, GParamSpec* /*pspec*/, gpointer userData) {
|
|
|
|
|
|
update_row_state(static_cast<KeyRow*>(userData));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void on_save_clicked(GtkButton* /*btn*/, gpointer /*userData*/) {
|
2026-10-17 02:57:21 +02:00
|
|
|
|
const std::string path = gkeymapper::default_config_path();
|
|
|
|
|
|
g_autofree gchar* dir = g_path_get_dirname(path.c_str());
|
|
|
|
|
|
g_mkdir_with_parents(dir, 0755);
|
2026-10-16 22:11:52 +02:00
|
|
|
|
|
2026-10-17 02:57:21 +02:00
|
|
|
|
if (!gkeymapper::write_mappings(path, rows)) {
|
2026-10-16 22:11:52 +02:00
|
|
|
|
g_printerr(_("Could not save %s.\n"), path.c_str());
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::cout << _("Mappings saved to ") << path << "\n";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void on_export_response(GtkDialog* dialog, int response, gpointer /*userData*/) {
|
|
|
|
|
|
if (response == GTK_RESPONSE_ACCEPT) {
|
|
|
|
|
|
GtkFileChooser* chooser = GTK_FILE_CHOOSER(dialog);
|
|
|
|
|
|
g_autoptr(GFile) file = gtk_file_chooser_get_file(chooser);
|
|
|
|
|
|
char* path = g_file_get_path(file);
|
|
|
|
|
|
if (path) {
|
2026-10-17 02:57:21 +02:00
|
|
|
|
if (!gkeymapper::write_mappings(path, rows)) {
|
2026-10-16 22:11:52 +02:00
|
|
|
|
g_printerr(_("Could not export mappings.\n"));
|
|
|
|
|
|
}
|
|
|
|
|
|
g_free(path);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
gtk_window_destroy(GTK_WINDOW(dialog));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void on_import_response(GtkDialog* dialog, int response, gpointer /*userData*/) {
|
|
|
|
|
|
if (response == GTK_RESPONSE_ACCEPT) {
|
|
|
|
|
|
GtkFileChooser* chooser = GTK_FILE_CHOOSER(dialog);
|
|
|
|
|
|
g_autoptr(GFile) file = gtk_file_chooser_get_file(chooser);
|
|
|
|
|
|
char* path = g_file_get_path(file);
|
|
|
|
|
|
if (path) {
|
2026-10-17 02:57:21 +02:00
|
|
|
|
gkeymapper::read_mappings(path, rows);
|
2026-10-16 22:11:52 +02:00
|
|
|
|
g_free(path);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
gtk_window_destroy(GTK_WINDOW(dialog));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static std::string find_asset(const std::string& name);
|
|
|
|
|
|
|
|
|
|
|
|
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_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");
|
2026-10-17 02:42:09 +02:00
|
|
|
|
gtk_about_dialog_set_website_label(GTK_ABOUT_DIALOG(dialog), "Thomas Rager - rager.dev");
|
2026-10-16 22:11:52 +02:00
|
|
|
|
|
|
|
|
|
|
std::string iconPath = find_asset("icon.png");
|
|
|
|
|
|
GError* error = nullptr;
|
|
|
|
|
|
GdkTexture* texture = gdk_texture_new_from_filename(iconPath.c_str(), &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);
|
|
|
|
|
|
gtk_window_present(GTK_WINDOW(dialog));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void do_export(GtkWindow* parent) {
|
|
|
|
|
|
GtkWidget* dialog = gtk_file_chooser_dialog_new(_("Export configuration"),
|
|
|
|
|
|
parent,
|
|
|
|
|
|
GTK_FILE_CHOOSER_ACTION_SAVE,
|
|
|
|
|
|
_("_Cancel"), GTK_RESPONSE_CANCEL,
|
|
|
|
|
|
_("_Save"), GTK_RESPONSE_ACCEPT,
|
|
|
|
|
|
NULL);
|
|
|
|
|
|
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "gkeymapper.conf");
|
|
|
|
|
|
g_signal_connect(dialog, "response", G_CALLBACK(on_export_response), NULL);
|
|
|
|
|
|
gtk_window_present(GTK_WINDOW(dialog));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void do_import(GtkWindow* parent) {
|
|
|
|
|
|
GtkWidget* dialog = gtk_file_chooser_dialog_new(_("Import configuration"),
|
|
|
|
|
|
parent,
|
|
|
|
|
|
GTK_FILE_CHOOSER_ACTION_OPEN,
|
|
|
|
|
|
_("_Cancel"), GTK_RESPONSE_CANCEL,
|
|
|
|
|
|
_("_Open"), GTK_RESPONSE_ACCEPT,
|
|
|
|
|
|
NULL);
|
|
|
|
|
|
g_signal_connect(dialog, "response", G_CALLBACK(on_import_response), NULL);
|
|
|
|
|
|
gtk_window_present(GTK_WINDOW(dialog));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void action_export(GSimpleAction* /*action*/, GVariant* /*parameter*/, gpointer userData) {
|
|
|
|
|
|
GtkApplication* app = GTK_APPLICATION(userData);
|
|
|
|
|
|
GtkWindow* window = gtk_application_get_active_window(app);
|
|
|
|
|
|
if (window) {
|
|
|
|
|
|
do_export(window);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void action_import(GSimpleAction* /*action*/, GVariant* /*parameter*/, gpointer userData) {
|
|
|
|
|
|
GtkApplication* app = GTK_APPLICATION(userData);
|
|
|
|
|
|
GtkWindow* window = gtk_application_get_active_window(app);
|
|
|
|
|
|
if (window) {
|
|
|
|
|
|
do_import(window);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void action_about(GSimpleAction* /*action*/, GVariant* /*parameter*/, gpointer userData) {
|
|
|
|
|
|
GtkApplication* app = GTK_APPLICATION(userData);
|
|
|
|
|
|
GtkWindow* window = gtk_application_get_active_window(app);
|
|
|
|
|
|
if (window) {
|
|
|
|
|
|
show_about_dialog(window);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static std::string find_asset(const std::string& name) {
|
|
|
|
|
|
const char* env = g_getenv("GKEYMAPPER_ASSETS_DIR");
|
|
|
|
|
|
if (env && *env) {
|
|
|
|
|
|
std::string path = std::string(env) + "/" + name;
|
|
|
|
|
|
if (g_file_test(path.c_str(), G_FILE_TEST_EXISTS)) {
|
|
|
|
|
|
return path;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string local = std::string("./assets/") + name;
|
|
|
|
|
|
if (g_file_test(local.c_str(), G_FILE_TEST_EXISTS)) {
|
|
|
|
|
|
return local;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#ifndef ASSETS_DIR
|
|
|
|
|
|
#define ASSETS_DIR "/usr/local/share/gkeymapper/assets"
|
|
|
|
|
|
#endif
|
|
|
|
|
|
std::string installed = std::string(ASSETS_DIR) + "/" + name;
|
|
|
|
|
|
if (g_file_test(installed.c_str(), G_FILE_TEST_EXISTS)) {
|
|
|
|
|
|
return installed;
|
|
|
|
|
|
}
|
|
|
|
|
|
return local;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void build_ui(GtkApplication* app) {
|
|
|
|
|
|
GtkWidget* window = gtk_application_window_new(app);
|
|
|
|
|
|
gtk_window_set_title(GTK_WINDOW(window), _("gKeyMapper – Logitech G910"));
|
|
|
|
|
|
gtk_window_set_resizable(GTK_WINDOW(window), FALSE);
|
|
|
|
|
|
|
|
|
|
|
|
GtkWidget* headerBar = gtk_header_bar_new();
|
|
|
|
|
|
gtk_window_set_titlebar(GTK_WINDOW(window), headerBar);
|
|
|
|
|
|
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());
|
|
|
|
|
|
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")));
|
|
|
|
|
|
gtk_header_bar_set_title_widget(GTK_HEADER_BAR(headerBar), titleBox);
|
|
|
|
|
|
|
|
|
|
|
|
GMenu* menu = g_menu_new();
|
|
|
|
|
|
g_menu_append(menu, _("Export configuration..."), "app.export-config");
|
|
|
|
|
|
g_menu_append(menu, _("Import configuration..."), "app.import-config");
|
|
|
|
|
|
|
|
|
|
|
|
GMenu* helpSection = g_menu_new();
|
|
|
|
|
|
g_menu_append(helpSection, _("About"), "app.about");
|
|
|
|
|
|
g_menu_append_section(menu, NULL, G_MENU_MODEL(helpSection));
|
|
|
|
|
|
|
|
|
|
|
|
GtkWidget* menuButton = gtk_menu_button_new();
|
|
|
|
|
|
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(menuButton), "open-menu-symbolic");
|
|
|
|
|
|
gtk_menu_button_set_menu_model(GTK_MENU_BUTTON(menuButton), G_MENU_MODEL(menu));
|
|
|
|
|
|
gtk_header_bar_pack_end(GTK_HEADER_BAR(headerBar), menuButton);
|
|
|
|
|
|
|
|
|
|
|
|
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"));
|
|
|
|
|
|
gtk_label_set_xalign(GTK_LABEL(subtitle), 0.0f);
|
|
|
|
|
|
gtk_widget_add_css_class(subtitle, "dim-label");
|
|
|
|
|
|
gtk_box_append(GTK_BOX(headerBox), subtitle);
|
|
|
|
|
|
|
|
|
|
|
|
// Table with all nine G keys (no scrolling needed, everything stays visible)
|
|
|
|
|
|
GtkWidget* grid = gtk_grid_new();
|
|
|
|
|
|
gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
|
|
|
|
|
|
gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
|
|
|
|
|
|
gtk_widget_set_margin_top(grid, 8);
|
|
|
|
|
|
gtk_widget_set_margin_bottom(grid, 4);
|
|
|
|
|
|
gtk_widget_set_margin_start(grid, 16);
|
|
|
|
|
|
gtk_widget_set_margin_end(grid, 16);
|
|
|
|
|
|
gtk_widget_set_hexpand(grid, TRUE);
|
|
|
|
|
|
gtk_box_append(GTK_BOX(outer), grid);
|
|
|
|
|
|
|
|
|
|
|
|
// Column headers
|
|
|
|
|
|
const char* headers[] = { _("G-Key"), _("Type"), _("Target (Key/Command)"), _("Action") };
|
|
|
|
|
|
for (int c = 0; c < 4; ++c) {
|
2026-10-17 02:57:21 +02:00
|
|
|
|
GtkWidget* h = gtk_label_new(nullptr);
|
|
|
|
|
|
g_autofree gchar* headerMarkup = g_strdup_printf("<b>%s</b>", headers[c]);
|
|
|
|
|
|
gtk_label_set_markup(GTK_LABEL(h), headerMarkup);
|
2026-10-16 22:11:52 +02:00
|
|
|
|
gtk_widget_add_css_class(h, "dim-label");
|
|
|
|
|
|
gtk_grid_attach(GTK_GRID(grid), h, c, 0, 1, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const char* typeOptions[] = { _("Key"), _("Command"), _("Disabled"), nullptr };
|
|
|
|
|
|
|
2026-10-17 02:57:21 +02:00
|
|
|
|
rows.resize(kNumKeys);
|
|
|
|
|
|
for (int i = 0; i < kNumKeys; ++i) {
|
2026-10-16 22:11:52 +02:00
|
|
|
|
rows[i].index = i;
|
|
|
|
|
|
|
|
|
|
|
|
// G key label
|
2026-10-17 02:57:21 +02:00
|
|
|
|
GtkWidget* keyLabel = gtk_label_new(nullptr);
|
|
|
|
|
|
g_autofree gchar* keyMarkup = g_strdup_printf("<b>G%d</b>", i + 1);
|
|
|
|
|
|
gtk_label_set_markup(GTK_LABEL(keyLabel), keyMarkup);
|
2026-10-16 22:11:52 +02:00
|
|
|
|
gtk_grid_attach(GTK_GRID(grid), keyLabel, 0, i + 1, 1, 1);
|
|
|
|
|
|
rows[i].keyLabel = keyLabel;
|
|
|
|
|
|
|
|
|
|
|
|
// Type dropdown (fixed width so rows don't jump between languages)
|
|
|
|
|
|
GtkWidget* drop = gtk_drop_down_new_from_strings(typeOptions);
|
|
|
|
|
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(drop), 0);
|
|
|
|
|
|
gtk_widget_set_size_request(drop, 150, -1);
|
|
|
|
|
|
gtk_grid_attach(GTK_GRID(grid), drop, 1, i + 1, 1, 1);
|
|
|
|
|
|
rows[i].typeDrop = drop;
|
|
|
|
|
|
|
|
|
|
|
|
g_signal_connect(drop, "notify::selected", G_CALLBACK(on_type_changed), &rows[i]);
|
|
|
|
|
|
|
|
|
|
|
|
// Target input (wide: commands and file paths need room)
|
|
|
|
|
|
GtkWidget* entry = gtk_entry_new();
|
2026-10-17 02:42:09 +02:00
|
|
|
|
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), _("e.g. ctrl+c, ctrl+v"));
|
2026-10-16 22:11:52 +02:00
|
|
|
|
gtk_widget_set_size_request(entry, 440, -1);
|
|
|
|
|
|
gtk_widget_set_hexpand(entry, TRUE);
|
|
|
|
|
|
gtk_grid_attach(GTK_GRID(grid), entry, 2, i + 1, 1, 1);
|
|
|
|
|
|
rows[i].targetEntry = entry;
|
|
|
|
|
|
|
|
|
|
|
|
// Capture button
|
|
|
|
|
|
GtkWidget* capture = gtk_button_new_with_label(_("Capture"));
|
|
|
|
|
|
gtk_widget_set_size_request(capture, 180, -1);
|
|
|
|
|
|
g_signal_connect(capture, "clicked", G_CALLBACK(on_capture_clicked), &rows[i]);
|
|
|
|
|
|
gtk_grid_attach(GTK_GRID(grid), capture, 3, i + 1, 1, 1);
|
|
|
|
|
|
rows[i].captureButton = capture;
|
|
|
|
|
|
|
|
|
|
|
|
update_row_state(&rows[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Footer with save button
|
|
|
|
|
|
GtkWidget* footer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
|
|
|
|
|
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* saveBtn = gtk_button_new_with_label(_("Save mappings"));
|
2026-10-17 02:42:09 +02:00
|
|
|
|
// 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.
|
2026-10-16 22:11:52 +02:00
|
|
|
|
g_signal_connect(saveBtn, "clicked", G_CALLBACK(on_save_clicked), nullptr);
|
|
|
|
|
|
gtk_box_append(GTK_BOX(footer), 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_window_present(GTK_WINDOW(window));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void on_activate(GtkApplication* app, gpointer /*user_data*/) {
|
|
|
|
|
|
GSimpleAction* exportAction = g_simple_action_new("export-config", NULL);
|
|
|
|
|
|
g_signal_connect(exportAction, "activate", G_CALLBACK(action_export), app);
|
|
|
|
|
|
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(exportAction));
|
|
|
|
|
|
|
|
|
|
|
|
GSimpleAction* importAction = g_simple_action_new("import-config", NULL);
|
|
|
|
|
|
g_signal_connect(importAction, "activate", G_CALLBACK(action_import), app);
|
|
|
|
|
|
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(importAction));
|
|
|
|
|
|
|
|
|
|
|
|
GSimpleAction* aboutAction = g_simple_action_new("about", NULL);
|
|
|
|
|
|
g_signal_connect(aboutAction, "activate", G_CALLBACK(action_about), app);
|
|
|
|
|
|
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(aboutAction));
|
|
|
|
|
|
|
|
|
|
|
|
build_ui(app);
|
|
|
|
|
|
}
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
int main(int argc, char** argv) {
|
|
|
|
|
|
setlocale(LC_ALL, "");
|
|
|
|
|
|
|
|
|
|
|
|
const char* localedir = g_getenv("GKEYMAPPER_LOCALEDIR");
|
|
|
|
|
|
if (!localedir || !*localedir) {
|
|
|
|
|
|
localedir = LOCALEDIR;
|
|
|
|
|
|
}
|
|
|
|
|
|
bindtextdomain(PACKAGE, localedir);
|
|
|
|
|
|
bind_textdomain_codeset(PACKAGE, "UTF-8");
|
|
|
|
|
|
textdomain(PACKAGE);
|
|
|
|
|
|
|
|
|
|
|
|
GtkApplication* app = gtk_application_new("com.example.gkeymapper",
|
|
|
|
|
|
G_APPLICATION_FLAGS_NONE);
|
|
|
|
|
|
g_signal_connect(app, "activate", G_CALLBACK(on_activate), nullptr);
|
|
|
|
|
|
int status = g_application_run(G_APPLICATION(app), argc, argv);
|
|
|
|
|
|
g_object_unref(app);
|
|
|
|
|
|
return status;
|
|
|
|
|
|
}
|