Nicer structure

This commit is contained in:
2026-10-17 02:57:21 +02:00
parent 99146577d4
commit 335a4674d1
10 changed files with 767 additions and 317 deletions

243
src/capturedialog.cpp Normal file
View File

@@ -0,0 +1,243 @@
// gKeyMapper - key sequence capture dialog.
#include "capturedialog.h"
#include <algorithm>
#include <cctype>
#include <set>
#include <string>
#include <vector>
#include <glib/gi18n.h>
namespace gkeymapper {
namespace {
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)
};
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").
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;
}
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;
}
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());
}
gboolean on_capture_key_pressed(GtkEventControllerKey* controller,
guint keyval, guint /*keycode*/,
GdkModifierType state, gpointer userData) {
auto* session = static_cast<CaptureSession*>(userData);
if (keyval == GDK_KEY_Escape) {
// Escape cancels the dialog; the window "destroy" handler frees the
// session, so return immediately and do not touch it afterwards.
gtk_window_destroy(GTK_WINDOW(gtk_widget_get_root(
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(controller)))));
return TRUE;
}
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;
}
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;
}
void on_capture_session_destroy(GtkWidget* /*widget*/, gpointer userData) {
delete static_cast<CaptureSession*>(userData);
}
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"
}
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"
}
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);
}
} // namespace
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);
g_autofree gchar* titleMarkup = g_strdup_printf(
"<span size='large' weight='bold'>%s</span>", _("Capture key sequence"));
gtk_label_set_markup(GTK_LABEL(title), titleMarkup);
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);
// The frame is the focusable recording area: keys are captured there while
// the buttons stay focusable so the dialog remains keyboard-navigable.
gtk_widget_set_focusable(frame, TRUE);
gtk_widget_set_focus_on_click(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 bubble from the focused recording frame to the window. When a
// button is focused it consumes Space/Return itself, so those activate the
// button instead of being recorded — the buttons stay fully usable.
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);
// Start recording immediately: focus the recording area, not a button.
gtk_window_set_focus(GTK_WINDOW(dialog), frame);
gtk_window_present(GTK_WINDOW(dialog));
}
} // namespace gkeymapper

17
src/capturedialog.h Normal file
View File

@@ -0,0 +1,17 @@
// gKeyMapper - modal dialog that records a key sequence for one row.
#ifndef GKEYMAPPER_CAPTUREDIALOG_H
#define GKEYMAPPER_CAPTUREDIALOG_H
#include <gtk/gtk.h>
#include "keyrow.h"
namespace gkeymapper {
// Opens the capture dialog; on "Done" the recorded sequence is written to
// row->targetEntry.
void open_key_capture_dialog(KeyRow* row, GtkWindow* parent);
} // namespace gkeymapper
#endif // GKEYMAPPER_CAPTUREDIALOG_H

22
src/keyrow.h Normal file
View File

@@ -0,0 +1,22 @@
// gKeyMapper - shared state for one G key row (G1..G9).
#ifndef GKEYMAPPER_KEYROW_H
#define GKEYMAPPER_KEYROW_H
#include <gtk/gtk.h>
namespace gkeymapper {
constexpr int kNumKeys = 9;
// Widgets belonging to a single G key row.
struct KeyRow {
int index = 0; // 0..8 -> G1..G9
GtkWidget* keyLabel = nullptr;
GtkWidget* typeDrop = nullptr;
GtkWidget* targetEntry = nullptr;
GtkWidget* captureButton = nullptr;
};
} // namespace gkeymapper
#endif // GKEYMAPPER_KEYROW_H

View File

@@ -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;

85
src/mappings.cpp Normal file
View File

@@ -0,0 +1,85 @@
// gKeyMapper - mappings configuration file I/O.
#include "mappings.h"
#include <fstream>
#include <string>
#include <glib/gi18n.h>
namespace gkeymapper {
std::string default_config_path() {
const char* home = g_get_home_dir();
return std::string(home ? home : "") + "/.config/gKeyMapper/mappings.conf";
}
bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows) {
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";
}
return true;
}
void read_mappings(const std::string& path, const std::vector<KeyRow>& rows) {
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 >= static_cast<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());
}
}
} // namespace gkeymapper

24
src/mappings.h Normal file
View File

@@ -0,0 +1,24 @@
// gKeyMapper - reading and writing the mappings configuration file.
#ifndef GKEYMAPPER_MAPPINGS_H
#define GKEYMAPPER_MAPPINGS_H
#include <string>
#include <vector>
#include "keyrow.h"
namespace gkeymapper {
// Default config file: ~/.config/gKeyMapper/mappings.conf.
std::string default_config_path();
// Writes all rows as "G1=type:target" lines. Returns false on I/O error.
bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows);
// Loads mappings from path into the rows' widgets. Missing/invalid lines are
// skipped; a missing file is reported and otherwise ignored.
void read_mappings(const std::string& path, const std::vector<KeyRow>& rows);
} // namespace gkeymapper
#endif // GKEYMAPPER_MAPPINGS_H