New locale

This commit is contained in:
2026-10-17 02:42:09 +02:00
parent d157d9d30d
commit 99146577d4
11 changed files with 614 additions and 458 deletions

View File

@@ -4,10 +4,12 @@
#include <gtk/gtk.h>
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <locale.h>
#include <libintl.h>
#include <set>
#include <string>
#include <vector>
@@ -60,20 +62,227 @@ 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));
GtkWindow* window = GTK_WINDOW(gtk_widget_get_root(GTK_WIDGET(btn)));
if (sel == 1) {
GtkWindow* window = GTK_WINDOW(gtk_widget_get_root(GTK_WIDGET(btn)));
pick_command_file(row, window);
return;
}
if (sel == 0) {
// Placeholder: real key capture will be added in a later version.
gtk_editable_set_text(GTK_EDITABLE(row->targetEntry), _("<Press a key...>"));
std::cout << "G" << (row->index + 1) << ": waiting for key...\n";
open_key_capture_dialog(row, window);
}
}
@@ -85,8 +294,12 @@ static void update_row_state(KeyRow* row) {
if (sel == 1) {
gtk_button_set_label(GTK_BUTTON(row->captureButton), _("Choose file..."));
gtk_entry_set_placeholder_text(GTK_ENTRY(row->targetEntry),
_("e.g. /usr/bin/firefox or custom command"));
} else {
gtk_button_set_label(GTK_BUTTON(row->captureButton), _("Capture"));
gtk_entry_set_placeholder_text(GTK_ENTRY(row->targetEntry),
_("e.g. ctrl+c, ctrl+v"));
}
gtk_widget_set_size_request(row->captureButton, 180, -1);
}
@@ -215,7 +428,7 @@ static void show_about_dialog(GtkWindow* parent) {
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), "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;
@@ -396,7 +609,7 @@ void build_ui(GtkApplication* app) {
// Target input (wide: commands and file paths need room)
GtkWidget* entry = gtk_entry_new();
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), _("e.g. ctrl+c or ls -la"));
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), _("e.g. ctrl+c, ctrl+v"));
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);
@@ -421,7 +634,9 @@ void build_ui(GtkApplication* app) {
gtk_box_append(GTK_BOX(outer), footer);
GtkWidget* saveBtn = gtk_button_new_with_label(_("Save mappings"));
gtk_widget_add_css_class(saveBtn, "suggested-action");
// 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);