Nicer structure
This commit is contained in:
332
src/main.cpp
332
src/main.cpp
@@ -1,18 +1,18 @@
|
||||
// gKeyMapper - GTK4 GUI for remapping the G keys (G1-G9) of the Logitech G910
|
||||
// gKeyMapper - GTK4 GUI for remapping the G keys (G1-G9) of the Logitech G910.
|
||||
// GUI only for now. Mappings are stored as a simple text file.
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <locale.h>
|
||||
#include <libintl.h>
|
||||
#include <set>
|
||||
#include <glib/gi18n.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "capturedialog.h"
|
||||
#include "keyrow.h"
|
||||
#include "mappings.h"
|
||||
|
||||
#ifndef PACKAGE
|
||||
#define PACKAGE "gkeymapper"
|
||||
#endif
|
||||
@@ -21,19 +21,10 @@
|
||||
#define LOCALEDIR "/usr/local/share/locale"
|
||||
#endif
|
||||
|
||||
#define _(s) gettext(s)
|
||||
#define N_(s) (s)
|
||||
|
||||
namespace {
|
||||
constexpr int NUM_KEYS = 9;
|
||||
|
||||
struct KeyRow {
|
||||
int index; // 0..8 -> G1..G9
|
||||
GtkWidget* keyLabel = nullptr;
|
||||
GtkWidget* typeDrop = nullptr;
|
||||
GtkWidget* targetEntry = nullptr;
|
||||
GtkWidget* captureButton = nullptr;
|
||||
};
|
||||
using gkeymapper::KeyRow;
|
||||
using gkeymapper::kNumKeys;
|
||||
|
||||
std::vector<KeyRow> rows;
|
||||
|
||||
@@ -62,215 +53,6 @@ static void pick_command_file(KeyRow* row, GtkWindow* parent) {
|
||||
gtk_window_present(GTK_WINDOW(dialog));
|
||||
}
|
||||
|
||||
struct CaptureSession {
|
||||
KeyRow* row = nullptr;
|
||||
GtkWidget* displayLabel = nullptr;
|
||||
std::vector<std::string> steps;
|
||||
std::set<guint> heldKeys; // non-modifier keyvals currently held (ignores auto-repeat)
|
||||
};
|
||||
|
||||
static bool is_modifier_keyval(guint keyval) {
|
||||
switch (keyval) {
|
||||
case GDK_KEY_Control_L:
|
||||
case GDK_KEY_Control_R:
|
||||
case GDK_KEY_Alt_L:
|
||||
case GDK_KEY_Alt_R:
|
||||
case GDK_KEY_Shift_L:
|
||||
case GDK_KEY_Shift_R:
|
||||
case GDK_KEY_Super_L:
|
||||
case GDK_KEY_Super_R:
|
||||
case GDK_KEY_Meta_L:
|
||||
case GDK_KEY_Meta_R:
|
||||
case GDK_KEY_ISO_Level3_Shift: // AltGr
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Format one step like the reference project: modifiers joined with '+',
|
||||
// e.g. "ctrl+shift+t". Multiple steps are joined with ',' ("ctrl+c,ctrl+v").
|
||||
static std::string format_capture_step(GdkModifierType state, guint keyval) {
|
||||
std::string step;
|
||||
if (state & GDK_CONTROL_MASK) {
|
||||
step += "ctrl+";
|
||||
}
|
||||
if (state & GDK_ALT_MASK) {
|
||||
step += "alt+";
|
||||
}
|
||||
if (state & GDK_SHIFT_MASK) {
|
||||
step += "shift+";
|
||||
}
|
||||
if (state & (GDK_SUPER_MASK | GDK_META_MASK)) {
|
||||
step += "super+";
|
||||
}
|
||||
const char* name = gdk_keyval_name(keyval);
|
||||
std::string key = name ? name : "?";
|
||||
std::transform(key.begin(), key.end(), key.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
step += key;
|
||||
return step;
|
||||
}
|
||||
|
||||
static std::string join_capture_steps(const std::vector<std::string>& steps) {
|
||||
std::string out;
|
||||
for (size_t i = 0; i < steps.size(); ++i) {
|
||||
if (i > 0) {
|
||||
out += ",";
|
||||
}
|
||||
out += steps[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static void update_capture_display(CaptureSession* session) {
|
||||
std::string text = join_capture_steps(session->steps);
|
||||
if (text.empty()) {
|
||||
text = _("No keys recorded yet");
|
||||
}
|
||||
gtk_label_set_text(GTK_LABEL(session->displayLabel), text.c_str());
|
||||
}
|
||||
|
||||
static gboolean on_capture_key_pressed(GtkEventControllerKey* /*controller*/,
|
||||
guint keyval, guint /*keycode*/,
|
||||
GdkModifierType state, gpointer userData) {
|
||||
auto* session = static_cast<CaptureSession*>(userData);
|
||||
if (is_modifier_keyval(keyval)) {
|
||||
return TRUE; // modifiers are recorded as part of the next real key
|
||||
}
|
||||
if (session->heldKeys.count(keyval)) {
|
||||
return TRUE; // ignore auto-repeat while the key is held down
|
||||
}
|
||||
session->heldKeys.insert(keyval);
|
||||
constexpr GdkModifierType kModMask = static_cast<GdkModifierType>(
|
||||
GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_ALT_MASK |
|
||||
GDK_SUPER_MASK | GDK_META_MASK);
|
||||
session->steps.push_back(format_capture_step(
|
||||
static_cast<GdkModifierType>(state & kModMask), keyval));
|
||||
update_capture_display(session);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean on_capture_key_released(GtkEventControllerKey* /*controller*/,
|
||||
guint keyval, guint /*keycode*/,
|
||||
GdkModifierType /*state*/, gpointer userData) {
|
||||
auto* session = static_cast<CaptureSession*>(userData);
|
||||
session->heldKeys.erase(keyval);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void on_capture_session_destroy(GtkWidget* /*widget*/, gpointer userData) {
|
||||
delete static_cast<CaptureSession*>(userData);
|
||||
}
|
||||
|
||||
static void on_capture_done_clicked(GtkButton* btn, gpointer userData) {
|
||||
auto* session = static_cast<CaptureSession*>(userData);
|
||||
if (!session->steps.empty()) {
|
||||
gtk_editable_set_text(GTK_EDITABLE(session->row->targetEntry),
|
||||
join_capture_steps(session->steps).c_str());
|
||||
}
|
||||
GtkWindow* window = GTK_WINDOW(gtk_widget_get_root(GTK_WIDGET(btn)));
|
||||
gtk_window_destroy(window); // triggers session cleanup via "destroy"
|
||||
}
|
||||
|
||||
static void on_capture_cancel_clicked(GtkButton* btn, gpointer /*userData*/) {
|
||||
GtkWindow* window = GTK_WINDOW(gtk_widget_get_root(GTK_WIDGET(btn)));
|
||||
gtk_window_destroy(window); // triggers session cleanup via "destroy"
|
||||
}
|
||||
|
||||
static void on_capture_clear_clicked(GtkButton* /*btn*/, gpointer userData) {
|
||||
auto* session = static_cast<CaptureSession*>(userData);
|
||||
session->steps.clear();
|
||||
session->heldKeys.clear();
|
||||
update_capture_display(session);
|
||||
}
|
||||
|
||||
static void open_key_capture_dialog(KeyRow* row, GtkWindow* parent) {
|
||||
auto* session = new CaptureSession();
|
||||
session->row = row;
|
||||
|
||||
GtkWidget* dialog = gtk_window_new();
|
||||
gtk_window_set_title(GTK_WINDOW(dialog), _("Capture key sequence"));
|
||||
gtk_window_set_transient_for(GTK_WINDOW(dialog), parent);
|
||||
gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
|
||||
gtk_window_set_resizable(GTK_WINDOW(dialog), FALSE);
|
||||
gtk_window_set_default_size(GTK_WINDOW(dialog), 540, 300);
|
||||
g_signal_connect(dialog, "destroy", G_CALLBACK(on_capture_session_destroy), session);
|
||||
|
||||
GtkWidget* outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
|
||||
gtk_widget_set_margin_top(outer, 20);
|
||||
gtk_widget_set_margin_bottom(outer, 20);
|
||||
gtk_widget_set_margin_start(outer, 20);
|
||||
gtk_widget_set_margin_end(outer, 20);
|
||||
gtk_window_set_child(GTK_WINDOW(dialog), outer);
|
||||
|
||||
GtkWidget* title = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(title),
|
||||
g_strdup_printf("<span size='large' weight='bold'>%s</span>",
|
||||
_("Capture key sequence")));
|
||||
gtk_label_set_xalign(GTK_LABEL(title), 0.0f);
|
||||
gtk_box_append(GTK_BOX(outer), title);
|
||||
|
||||
GtkWidget* info = gtk_label_new(_("Press keys in order (modifiers like Ctrl, Alt, Shift and Super are recorded too). Click Done to apply or Cancel to discard."));
|
||||
gtk_label_set_wrap(GTK_LABEL(info), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(info), 0.0f);
|
||||
gtk_widget_add_css_class(info, "dim-label");
|
||||
gtk_box_append(GTK_BOX(outer), info);
|
||||
|
||||
session->displayLabel = gtk_label_new(_("No keys recorded yet"));
|
||||
gtk_label_set_xalign(GTK_LABEL(session->displayLabel), 0.0f);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(session->displayLabel), PANGO_ELLIPSIZE_MIDDLE);
|
||||
gtk_widget_add_css_class(session->displayLabel, "monospace");
|
||||
gtk_widget_set_margin_top(session->displayLabel, 12);
|
||||
gtk_widget_set_margin_bottom(session->displayLabel, 12);
|
||||
gtk_widget_set_margin_start(session->displayLabel, 12);
|
||||
gtk_widget_set_margin_end(session->displayLabel, 12);
|
||||
GtkWidget* frame = gtk_frame_new(nullptr);
|
||||
gtk_frame_set_child(GTK_FRAME(frame), session->displayLabel);
|
||||
gtk_widget_set_size_request(frame, -1, 64);
|
||||
gtk_widget_set_vexpand(frame, TRUE);
|
||||
gtk_box_append(GTK_BOX(outer), frame);
|
||||
|
||||
gtk_box_append(GTK_BOX(outer), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
// Buttons: Clear on the left, Cancel/Done grouped on the right.
|
||||
GtkWidget* buttonRow = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_box_append(GTK_BOX(outer), buttonRow);
|
||||
|
||||
GtkWidget* clearBtn = gtk_button_new_with_label(_("Clear"));
|
||||
gtk_widget_set_size_request(clearBtn, 110, -1);
|
||||
g_signal_connect(clearBtn, "clicked", G_CALLBACK(on_capture_clear_clicked), session);
|
||||
gtk_box_append(GTK_BOX(buttonRow), clearBtn);
|
||||
|
||||
GtkWidget* spacer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_set_hexpand(spacer, TRUE);
|
||||
gtk_box_append(GTK_BOX(buttonRow), spacer);
|
||||
|
||||
GtkWidget* endBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_box_append(GTK_BOX(buttonRow), endBox);
|
||||
|
||||
GtkWidget* cancelBtn = gtk_button_new_with_label(_("Cancel"));
|
||||
gtk_widget_set_size_request(cancelBtn, 110, -1);
|
||||
g_signal_connect(cancelBtn, "clicked", G_CALLBACK(on_capture_cancel_clicked), nullptr);
|
||||
gtk_box_append(GTK_BOX(endBox), cancelBtn);
|
||||
|
||||
GtkWidget* doneBtn = gtk_button_new_with_label(_("Done"));
|
||||
gtk_widget_set_size_request(doneBtn, 110, -1);
|
||||
g_signal_connect(doneBtn, "clicked", G_CALLBACK(on_capture_done_clicked), session);
|
||||
gtk_box_append(GTK_BOX(endBox), doneBtn);
|
||||
|
||||
// Key events go to the dialog; buttons must not steal Space/Return.
|
||||
GtkEventController* keyCtrl = gtk_event_controller_key_new();
|
||||
g_signal_connect(keyCtrl, "key-pressed", G_CALLBACK(on_capture_key_pressed), session);
|
||||
g_signal_connect(keyCtrl, "key-released", G_CALLBACK(on_capture_key_released), session);
|
||||
gtk_widget_add_controller(dialog, keyCtrl);
|
||||
gtk_widget_set_focusable(clearBtn, FALSE);
|
||||
gtk_widget_set_focusable(cancelBtn, FALSE);
|
||||
gtk_widget_set_focusable(doneBtn, FALSE);
|
||||
|
||||
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));
|
||||
@@ -282,7 +64,7 @@ void on_capture_clicked(GtkButton* btn, gpointer userData) {
|
||||
}
|
||||
|
||||
if (sel == 0) {
|
||||
open_key_capture_dialog(row, window);
|
||||
gkeymapper::open_key_capture_dialog(row, window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,34 +90,12 @@ static void on_type_changed(GObject* /*object*/, GParamSpec* /*pspec*/, gpointer
|
||||
update_row_state(static_cast<KeyRow*>(userData));
|
||||
}
|
||||
|
||||
static bool write_mappings_to_file(const std::string& path) {
|
||||
std::ofstream out(path);
|
||||
if (!out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out << "# gKeyMapper mappings (G1..G9)\n";
|
||||
for (const auto& row : rows) {
|
||||
const char* type = "disabled";
|
||||
switch (gtk_drop_down_get_selected(GTK_DROP_DOWN(row.typeDrop))) {
|
||||
case 0: type = "key"; break;
|
||||
case 1: type = "command"; break;
|
||||
default: type = "disabled"; break;
|
||||
}
|
||||
const char* target = gtk_editable_get_text(GTK_EDITABLE(row.targetEntry));
|
||||
out << "G" << (row.index + 1) << "=" << type << ":" << (target ? target : "") << "\n";
|
||||
}
|
||||
out.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
void on_save_clicked(GtkButton* /*btn*/, gpointer /*userData*/) {
|
||||
const char* home = g_get_home_dir();
|
||||
std::string dir = std::string(home) + "/.config/gKeyMapper";
|
||||
g_mkdir_with_parents(dir.c_str(), 0755);
|
||||
std::string path = dir + "/mappings.conf";
|
||||
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);
|
||||
|
||||
if (!write_mappings_to_file(path)) {
|
||||
if (!gkeymapper::write_mappings(path, rows)) {
|
||||
g_printerr(_("Could not save %s.\n"), path.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -343,61 +103,13 @@ void on_save_clicked(GtkButton* /*btn*/, gpointer /*userData*/) {
|
||||
std::cout << _("Mappings saved to ") << path << "\n";
|
||||
}
|
||||
|
||||
static void import_mappings_from_file(const std::string& path) {
|
||||
std::ifstream in(path);
|
||||
if (!in) {
|
||||
g_printerr(_("Could not open %s.\n"), path.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
if (line.empty() || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
auto eq = line.find('=');
|
||||
if (eq == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
std::string key = line.substr(0, eq);
|
||||
if (key.size() < 2 || key[0] != 'G') {
|
||||
continue;
|
||||
}
|
||||
int n = 0;
|
||||
try {
|
||||
n = std::stoi(key.substr(1)) - 1;
|
||||
} catch (...) {
|
||||
continue;
|
||||
}
|
||||
if (n < 0 || n >= (int)rows.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string rest = line.substr(eq + 1);
|
||||
auto col = rest.find(':');
|
||||
std::string typeToken = (col == std::string::npos) ? rest : rest.substr(0, col);
|
||||
std::string target = (col == std::string::npos) ? std::string() : rest.substr(col + 1);
|
||||
|
||||
guint idx = 2;
|
||||
if (typeToken == "key") {
|
||||
idx = 0;
|
||||
} else if (typeToken == "command") {
|
||||
idx = 1;
|
||||
} else if (typeToken == "disabled") {
|
||||
idx = 2;
|
||||
}
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(rows[n].typeDrop), idx);
|
||||
gtk_editable_set_text(GTK_EDITABLE(rows[n].targetEntry), target.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!write_mappings_to_file(path)) {
|
||||
if (!gkeymapper::write_mappings(path, rows)) {
|
||||
g_printerr(_("Could not export mappings.\n"));
|
||||
}
|
||||
g_free(path);
|
||||
@@ -412,7 +124,7 @@ static void on_import_response(GtkDialog* dialog, int response, gpointer /*userD
|
||||
g_autoptr(GFile) file = gtk_file_chooser_get_file(chooser);
|
||||
char* path = g_file_get_path(file);
|
||||
if (path) {
|
||||
import_mappings_from_file(path);
|
||||
gkeymapper::read_mappings(path, rows);
|
||||
g_free(path);
|
||||
}
|
||||
}
|
||||
@@ -580,21 +292,23 @@ void build_ui(GtkApplication* app) {
|
||||
// Column headers
|
||||
const char* headers[] = { _("G-Key"), _("Type"), _("Target (Key/Command)"), _("Action") };
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
GtkWidget* h = gtk_label_new(headers[c]);
|
||||
gtk_label_set_markup(GTK_LABEL(h), g_strdup_printf("<b>%s</b>", headers[c]));
|
||||
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);
|
||||
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 };
|
||||
|
||||
rows.resize(NUM_KEYS);
|
||||
for (int i = 0; i < NUM_KEYS; ++i) {
|
||||
rows.resize(kNumKeys);
|
||||
for (int i = 0; i < kNumKeys; ++i) {
|
||||
rows[i].index = i;
|
||||
|
||||
// G key label
|
||||
GtkWidget* keyLabel = gtk_label_new(g_strdup_printf("G%d", i + 1));
|
||||
gtk_label_set_markup(GTK_LABEL(keyLabel), g_strdup_printf("<b>G%d</b>", i + 1));
|
||||
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);
|
||||
gtk_grid_attach(GTK_GRID(grid), keyLabel, 0, i + 1, 1, 1);
|
||||
rows[i].keyLabel = keyLabel;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user